Redis做分布式锁这事儿,怎么实现才靠谱又简单点
- 问答
- 2026-01-11 20:07:40
- 2
关于用Redis做分布式锁这件事,想做得既靠谱又简单,关键不在于使用最复杂的命令,而在于理解清楚锁的本质和可能出问题的地方,然后有针对性地解决,网上有很多讨论,比如Redis官网的[Distributed Locks with Redis]文档和马丁·克莱普曼(Martin Kleppmann)的文章[How to do distributed locking],都深入探讨了这个问题,我们不纠结于专业术语,就用人话把这事儿说明白。
锁的核心是什么?
想象一下,一个公共卫生间,门上有个简单的插销,锁的核心功能就是:
- 互斥性:一个人进去把插销插上,其他人就从外面打不开了,这叫互斥访问。
- 安全性:这个插销必须结实,不会自己弹开,锁在持有期间不能被其他人意外释放或失效。
- 可用性:上厕所的人不能一直占着茅坑不拉屎,他完事儿后得出来,把插销打开,让下一个人能用,锁最终一定要被释放,不能造成死锁。
Redis做分布式锁,就是用一个Redis的键(Key)来代表这个“插销”。
最简单的起步:SETNX 命令的误区
最早大家常用SETNX(SET if Not eXists)命令,如果键不存在,它就设置这个键,表示加锁成功;如果键已存在,就失败,表示锁被别人拿着,这解决了互斥性问题。
但这个方法问题很大,非常不靠谱:
- 死锁风险:如果客户端加锁成功后,还没来得及释放锁就崩溃了,那么这个锁就永远存在了,其他客户端再也拿不到锁,系统就“死”了。
- 误删风险:客户端A加锁成功,执行时间过长,锁还未来得及释放就过期了(如果设置了过期时间),这时客户端B成功加锁,然后客户端A执行完了,把锁给删了,但删的是客户端B的锁!这就乱套了。
单纯用SETNX是绝对不行的。
靠谱又简单的实现:单命令原子操作
Redis 2.6.12之后,提供了一个强大的命令:SET key value NX PX milliseconds,这个命令一次性解决了设置值和设置过期时间两个动作,而且是原子性的,不会分开执行,这就靠谱多了。
具体步骤是:
-
加锁:
- 客户端生成一个唯一的随机值(比如UUID),作为锁的“指纹”。
- 执行命令:
SET lock_key 唯一的随机值 NX PX 30000 - 解释:尝试设置一个键为
lock_key的锁,只有在这个键不存在(NX)时才设置成功,并且给这个锁设置一个30秒的过期时间(PX 30000)。 value值就是我们生成的唯一随机值。
-
执行业务逻辑:
- 如果上一步返回成功,说明加锁成功,就可以安心地去执行那些需要“互斥”访问的业务代码了。
- 如果返回失败,说明锁已被其他客户端持有,可以选择重试或直接返回失败。
-
释放锁:这是最关键的一步,必须保证只能释放自己持有的锁。
- 使用Lua脚本,因为Lua脚本在Redis中是原子执行的,可以避免并发问题。
- 脚本逻辑很简单:先获取
lock_key对应的值,如果这个值和我当初设置的那个“唯一的随机值”相等,那我就删除这个锁;如果不相等,说明这个锁已经不属于我了(可能已经过期并被其他客户端获取),那我就什么都不做。 - 伪代码看起来像这样:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - 通过这个脚本,就完美解决了“误删”别人锁的问题。
如何更上一层楼?解决超时问题
上面这个方法已经能应对90%的场景了,但它还有一个经典难题:客户端超时。
假设客户端A加锁成功,开始处理业务,但业务处理的时间超过了锁的过期时间(比如设置的30秒,但业务处理了40秒),那么在第30秒时,Redis会自动把锁删除,客户端B就能成功加锁,这样一来,就有两个客户端同时进入了临界区,锁的互斥性就被破坏了。
对于这个问题,没有完美的银弹,但有一些常见的应对策略,让方案更健壮:
- 设置合理的过期时间:这需要你对业务代码的执行时间有充分的评估,宁可设长一点,也别设短了,比如平均执行时间1秒,你可以设个10秒或20秒,留足余量。
- 使用“看门狗”机制(简单版):这是更靠谱的做法,思路是,在客户端加锁成功后,另起一个后台线程,这个线程定时(比如每隔10秒)去检查一下锁是否还存在且还是我的,如果还是我的,就自动给锁“续命”,延长它的过期时间,这样,只要客户端还“活着”并且在正常处理业务,锁就不会因为超时而被释放,当客户端业务处理完,主动释放锁的同时,也要停掉这个看门狗线程,一些成熟的Redis客户端(如Redisson)已经内置了这个功能。
- 快速失败的设计:在设计业务时,就要有意识,如果某个操作持有锁的时间可能很长,风险很高,可以考虑是否能用消息队列等方式解耦,或者将操作设计成幂等的(即使偶尔锁失效了,重复执行也不会造成严重后果)。
一个靠谱又简单的Redis分布式锁实现应该是这样的:
- 加锁:使用一条原子命令
SET lock_name unique_value NX PX timeout。 - 释放锁:使用比较value值是否相等的Lua脚本原子删除。
- 容错:考虑使用“看门狗”或设置合理超时时间来避免业务未完成锁却过期的问题。
最后要提醒的是,Redis的这种分布式锁在大多数情况下已经足够好用了,但它并不是一个强一致性的锁,在极端情况下(如Redis主从切换时发生脑裂),锁的安全性可能会被打破,如果你需要百分之百绝对可靠的锁,可能需要考虑ZooKeeper或etcd等一致性协议更强的系统,但对于绝大多数应用场景,遵循上述原则实现的Redis分布式锁,在简单性和可靠性之间已经取得了非常好的平衡。

本文由雪和泽于2026-01-11发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/78885.html
