用Redis怎么搞分布式乐观锁,感觉挺有意思也不难理解
- 问答
- 2026-01-12 22:58:24
- 2
说到用Redis搞分布式乐观锁,这个想法确实挺有意思,而且它的核心思想一点也不复杂,咱们可以把它想象成一个大家都能看到的公共“小黑板”,这个锁的精髓不在于“霸占”,而在于“验证”。
核心思想:版本号就是一切
乐观锁,顾名思义,它很“乐观”,它假设大部分情况下,不会有好几个人同时去修改同一个东西,它不会像悲观锁那样,一上来就把资源锁死,不让别人碰,相反,它允许任何人都可以来读取数据,甚至在你修改的时候,别人也能读取和修改他们自己的副本。
那怎么保证不出错呢?关键就在于一个“版本号”,你可以把这个版本号理解成贴在数据上的一个“标签”,每次数据被成功修改后,这个标签就换一个新的、更大的号码。
这个过程,完全可以用Redis轻松实现,因为Redis的SETNX命令和Lua脚本简直就是为这个场景量身定做的,下面我们一步步拆开来看。
第一步:先读数据和版本号
假设我们有一个商品库存,键叫 item:1001,它的库存数量是 100,我们怎么知道这个100是不是最新的呢?我们给它配一个版本号键,item:1001:version,初始值是 1。
当你想减库存时,比如买一件商品,你首先需要把当前的库存值(100)和当前的版本号(1)一起读出来,这一步很简单,就是用Redis的 GET 命令分别获取这两个键的值。
第二步:本地计算,准备修改
你在你的程序里进行业务计算,新库存 = 100 - 1 = 99,这个计算是在你的应用服务器上完成的,Redis并不知道。
第三步:提交修改,关键一步——检查版本
这是最核心的一步,你现在要告诉Redis:“请把 item:1001 的值改成99,前提是它的版本号必须还是我之前读到的那个1,如果版本号变了,说明有别人在我之前改过了,那你就别执行这个操作。”
在Redis里,我们怎么实现这个“前提”呢?有几种方法,最常用、最可靠的是用 Lua脚本。
一个简单的Lua脚本是这样的:
local currentVersion = redis.call('GET', KEYS[2])
if currentVersion == ARGV[1] then
-- 版本号对得上,说明在我读取之后没人修改过
redis.call('SET', KEYS[1], ARGV[2]) -- 设置新库存值
redis.call('INCR', KEYS[2]) -- 将版本号加1
return true -- 返回成功
else
return false -- 返回失败,版本号不一致
end
这个脚本的执行是原子性的,也就是说,Redis保证在执行这个脚本的时候,不会被其他命令打断,这是实现乐观锁安全性的关键。
我们来模拟一下两种场景:
- 场景A(没有冲突): 你执行脚本,传入旧的版本号
1,Redis一检查,发现当前的版本号确实是1,于是它放心地把库存改为99,并把版本号增加为2,返回成功,你的事务完成了。 - 场景B(发生冲突): 在你读取版本号
1之后,准备提交之前,另一个人手更快,他已经成功地修改了库存,并把版本号变成了2,这时你再执行脚本,传入旧的版本号1,Redis一检查,发现当前版本号是2,和你的1对不上,于是它拒绝执行修改,返回失败,你的事务就失败了。
第四步:根据结果处理
如果第三步返回成功,恭喜你,操作顺利完成,如果返回失败,就意味着发生了“写冲突”,这时候你该怎么办?乐观锁的标准处理方式是:重试。
你可以选择重新开始整个流程:再次读取最新的库存和版本号,在本地重新计算新库存(因为此时的库存可能已经不是99了,比如被别人买到了98),然后再次执行那个Lua脚本,这个过程可以重复几次,直到成功或者超过重试次数后报错给用户。
除了Lua脚本,还有别的招吗?
有,但没那么完美,比如可以用 WATCH / MULTI / EXEC 组合。
WATCH key:你可以把它理解为“盯住”版本号这个键。- 读取数据和版本号。
- 开启一个事务(
MULTI)。 - 在事务里发送你的修改命令(
SET库存,INCR版本号)。 - 尝试提交事务(
EXEC)。
Redis会在执行 EXEC 时检查,如果从 WATCH 开始到 EXEC 的这段时间里,你“盯住”的那个版本号键被其他客户端修改过了,那么整个事务就会失败,返回 nil。
这个方法也能实现乐观锁,但它有个小缺点:如果冲突很频繁,会导致很多事务失败,浪费资源,而Lua脚本是在服务端一次性完成检查和写入,更高效一些,所以现在大家更倾向于直接用Lua脚本。
总结一下Redis分布式乐观锁的优缺点
-
优点:
- 简单直观: 核心逻辑就是比版本号,很容易理解。
- 高性能: 在冲突不激烈的场景下,避免了传统锁的等待开销,并发性很好。
- 避免死锁: 因为它根本不会长期占有锁,都是短平快的操作,所以没有死锁问题。
-
缺点:
- 冲突处理: 一旦冲突发生,业务层需要有“重试”机制,这增加了程序的复杂性。
- 不保证绝对公平: 可能会出现某个请求一直重试但总被别的请求抢先的情况(饥饿)。
- 适用场景有限: 非常适合像“秒杀扣库存”、“更新用户积分”这种冲突不那么剧烈的场景,但如果某个数据被超高并发地修改(比如热点商品),重试会非常频繁,效率反而会下降,这时候可能就需要其他方案了(比如悲观锁、队列等)。
用Redis搞分布式乐观锁,就像是在一个开放的操场上玩游戏,大家遵守同一个规则:修改前先看一眼版本号,如果发现世界已经变了,那就坦然接受失败,然后从头再来一次,这种轻量级、非阻塞的思想,正是它在很多分布式系统中广受欢迎的原因。

本文由黎家于2026-01-12发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/79580.html
