Redis锁怎么用才靠谱,写法和注意点都得知道,不然容易出问题
- 问答
- 2025-12-27 09:43:45
- 4
Redis锁怎么用才靠谱,写法和注意点都得知道,不然容易出问题
直接用Redis来实现一个分布式锁,听起来很简单,不就是用一个Key来占坑嘛,但真要把它用在生产环境,确保业务数据不出错,里面有不少坑需要避开,一个不靠谱的锁,轻则导致数据混乱,重则可能让整个服务瘫痪,下面就来详细说说怎么用Redis锁才靠谱。
基础写法:从“setnx”到“set”命令的进化
最早,大家习惯用SETNX命令(SET if Not eXists)来加锁,这个命令的意思是,只有当这个key不存在时,才能设置成功,你想给“订单号123”这个资源加锁,可能会这么写:
SETNX lock:order:123 1
如果返回1,说明加锁成功,你可以去处理订单123了,处理完后,再用DEL命令删除这个key,也就是释放锁。
但这种方法问题很大:
- 死锁风险:如果加锁成功的服务在释放锁之前突然崩溃了,那么这个锁就永远无法被释放,其他服务再也拿不到锁,形成死锁。
- 误删别人锁的风险:服务A加锁成功,但业务处理时间过长,超过了我们心里预期的锁持有时间,这时,锁可能因为超时被Redis自动删除,或者被一个不严谨的守护线程删除,服务B趁机加锁成功,紧接着,服务A处理完,执行了
DEL命令,结果把服务B的锁给删了,这会导致锁完全失效。
为了解决死锁问题,人们想到了给锁加一个过期时间,一开始的写法可能是:
SETNX lock:order:123 1
EXPIRE lock:order:123 10
但这又引入了新问题:这两条命令不是原子的,如果执行完SETNX后,服务崩溃了,没来得及执行EXPIRE,锁依然不会自动过期,死锁还是会发生。
靠谱的第一步是:使用原子性命令同时完成设值和设置过期时间。 Redis 2.6.12版本之后,SET命令增加了扩展参数,可以完美解决这个问题。(来源:Redis官方文档SET命令选项)

推荐的加锁命令是:
SET lock:your_resource_name a_random_value NX PX 30000
解释一下:
NX:和SETNX效果一样,只有key不存在时才设置。PX 30000:设置key的过期时间为30000毫秒(30秒)。a_random_value:这是一个非常重要的部分,它是一个随机生成的、唯一的值(比如UUID),它的作用我们下面会讲。
这个命令是原子执行的,要么一起成功,要么一起失败,确保了加锁和设置过期时间的操作不可分割。
关键注意点:释放锁的正确姿势
加锁搞定了,释放锁更不能马虎,直接DEL key的风险前面已经说了,可能会误删其他客户端持有的锁。
靠谱的释放锁逻辑是:只能删除自己设置的锁。 这就需要用到刚才设置的a_random_value。

释放锁需要三步,但为了保证原子性,我们需要用Lua脚本来实现:
- 获取锁当前的值。
- 判断当前的值是否等于自己加锁时设置的那个随机值。
- 如果相等,则删除锁(释放);如果不相等,说明锁已经不属于自己,什么都不做。
用Lua脚本是因为Redis会单线程执行整个脚本,确保这三步操作是原子的,不会被其他命令打断。
Lua脚本示例:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
释放锁时,客户端会传递两个参数:KEYS[1]是锁的key(如lock:order:123),ARGV[1]是自己当初设置的那个随机值。
这一步是避免锁被误删的核心,必须实现。(这种模式常被称为“令牌锁”或“随机值验证锁”)
高级问题与考量

即使做到了以上两点,在一些极端情况下还是可能出问题,Martin Kleppmann在他的论文《How to do distributed locking》中深入讨论过。
一个典型场景是锁过期而任务未完成。
- 客户端A获取锁,设置30秒过期。
- 客户端A因为某些原因(如GC垃圾回收、网络延迟)导致业务处理时间超过30秒。
- Redis节点认为锁已超时,自动将其删除。
- 客户端B此时成功获取到锁。
- 客户端A终于处理完任务,执行释放锁的Lua脚本,但此时锁已经是B的了,由于有随机值保护,A不会删掉B的锁,这很好。但问题在于,A和B现在都认为自己持有锁,并且可能同时在操作共享资源,导致数据出错。
如何缓解这个问题?
- 设置合理的过期时间:过期时间不能太短,必须远大于业务代码的平均执行时间,预留出足够的缓冲,但这只是一种估计,无法根本解决。
- 使用看门狗(Watchdog)机制:这是Redisson等成熟Redis客户端库采用的方法,客户端在获取锁成功后,会启动一个后台线程,定时(比如在过期时间的1/3时)去检查锁是否还持有,如果还持有,就自动延长锁的过期时间,这样只要客户端还“活着”,且业务没执行完,锁就不会因为超时而被释放,这大大降低了业务未完成锁先超时的概率。(来源:Redisson分布式锁实现原理)
- 业务层面做幂等和校验:这是最后一道防线,即使锁真的失效了,你的业务逻辑也应该尽可能设计成允许多次处理而不出错(幂等),或者在有并发冲突时能有数据校验机制发现不一致。
选择可靠的Redis部署模式
我们上面的讨论都基于单个Redis实例,如果这个Redis实例宕机了,那么整个锁服务就不可用了,有人会想用Redis的主从复制(Replication)来解决,但这会引入新的问题。
在主从切换的场景下:
- 客户端A向主节点申请锁成功。
- 主节点在将锁信息同步给从节点之前,突然宕机了。
- 从节点被提升为新的主节点。
- 客户端B向新的主节点申请锁,因为新主节点上没有锁信息,所以B也能申请成功。
- A和B又同时持有了锁,违反了锁的互斥性。
为了解决这个问题,Redis的作者提出了RedLock算法。 它要求客户端向一个独立的Redis实例集群(比如5个)中的大多数(N/2+1)实例依次申请锁,只有当从大多数实例上都获取到锁,才算加锁成功,这样可以容忍少数实例的故障,但RedLock算法本身争议较大,实现复杂,且对时钟漂移敏感,一般建议在需要极高可靠性的场景下,并且充分理解其优缺点后才考虑使用,对于绝大多数应用场景,使用单个Redis实例(配合合理的过期时间和看门狗)或者Redis哨兵(Sentinel)模式已经足够,并优先通过业务设计来降低对锁绝对正确的依赖。
总结一下靠谱使用Redis锁的要点:
- 使用SET命令带NX和PX选项,原子性地加锁和设置过期时间。
- 锁的值必须使用唯一随机数,用于标识客户端身份。
- 释放锁时使用Lua脚本,原子性地验证随机数并删除锁。
- 设置一个远大于业务处理时间的过期时间,并考虑使用看门狗机制自动续期。
- 理解Redis不同部署模式(单机、主从、集群)下锁的可靠性限制,根据业务场景选择。
- 不要过度依赖分布式锁,能在业务逻辑层面做处理(如乐观锁、状态机)是更好的选择。
直接手写实现一个生产级的Redis锁需要考虑很多细节,因此在实际项目中,更推荐使用像Redisson这样经过充分测试的客户端库,它已经优雅地封装了上述所有最佳实践。
本文由革姣丽于2025-12-27发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/69339.html
