Redis 的 SETNX 实现分布式锁有哪些坑

在分布式系统中,锁是绕不开的话题。Redis 凭借其高性能和简单易用的特性,成为实现分布式锁最常用的方案之一。而 SETNX(SET if Not eXists)命令,作为 Redis 分布式锁的基石,看似简单,实则暗藏玄机。很多人在面试中被问到“如何用 Redis 实现分布式锁”时,都能说出 SETNX + EXPIRE 的思路,但真正深挖下去,会发现这里面坑一个接一个。本文就来系统梳理一下,用 SETNX 实现分布式锁到底有哪些坑,以及如何规避。

一、原子性陷阱:SETNX + EXPIRE 不是原子操作

最经典的错误写法是这样的:

SETNX lock_key unique_value
EXPIRE lock_key 10

逻辑上没问题:如果 key 不存在就设置成功,然后给 key 设置过期时间,防止死锁。但问题在于,SETNXEXPIRE 是两条独立的命令,中间可能发生故障。

假设客户端 A 执行 SETNX 成功,但在执行 EXPIRE 之前,客户端崩溃了,或者 Redis 发生主从切换、网络抖动,导致 EXPIRE 没有执行。那么这个 lock_key 就会永远存在,其他客户端永远无法获取锁,形成死锁

正确做法:使用 Redis 2.6.12 及以上版本支持的 SET 命令的扩展参数:

SET lock_key unique_value NX EX 10

这一条命令同时完成了“不存在则设置”和“设置过期时间”两个操作,保证了原子性。

二、锁误删陷阱:谁动了我的锁?

假设客户端 A 获取了锁,过期时间 10 秒。但由于业务逻辑执行时间较长,10 秒后锁自动释放了。此时客户端 B 获取到了锁。接着客户端 A 业务执行完毕,执行 DEL lock_key 释放锁——但它删除的其实是客户端 B 持有的锁!

这就是锁误删问题。解决方案是:在 SET 时给 value 设置一个唯一标识(比如 UUID + 线程 ID),释放锁时先判断 value 是否是自己设置的,再决定是否删除。

但这里又有一个坑:判断和删除是两步操作,不具备原子性。如果在判断之后、删除之前,锁刚好过期并被别人获取,依然会误删。

正确做法:使用 Lua 脚本保证判断和删除的原子性:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

三、过期时间陷阱:业务没执行完,锁就没了

即使解决了误删问题,还有一个根本性矛盾:过期时间设多长?

  • 设短了:业务还没执行完,锁自动释放,其他客户端乘虚而入,导致并发问题。
  • 设长了:如果客户端崩溃,锁长时间无法释放,影响可用性。

这是一个两难。更麻烦的是,即使你估算了一个“足够长”的时间,也可能因为 GC 停顿、网络延迟、系统负载等原因,导致业务执行时间超出预期。

解决方案锁续期(Watch Dog 机制)。获取锁成功后,启动一个后台线程(或定时任务),定期检查业务是否还在执行,如果是,则延长锁的过期时间。Redisson 框架就内置了这种看门狗机制,默认每 10 秒续期一次,续期到 30 秒。

但续期本身也有坑:如果续期线程因为 GC 或网络问题没能及时续期,锁还是会过期。所以续期机制只能降低风险,不能完全消除。

四、可重入性陷阱:同一个线程能再次加锁吗?

SETNX 实现的锁是不可重入的。同一个线程如果再次调用加锁逻辑,会因为 key 已存在而失败,导致自己把自己锁死。

在复杂的业务逻辑中,一个方法可能调用另一个也需要加锁的方法,这时候就需要可重入锁。用 SETNX 实现可重入锁,需要额外维护一个计数器(比如用 Hash 结构存储线程 ID 和重入次数),实现复杂度大幅上升。

建议:如果确实需要可重入锁,直接使用 Redisson 等成熟框架,不要自己造轮子。

五、主从切换陷阱:锁丢了怎么办?

这是 Redis 分布式锁最致命的坑。Redis 主从架构下,数据是异步复制的。假设:

  1. 客户端 A 在主节点获取了锁。
  2. 主节点在将锁数据同步到从节点之前,宕机了。
  3. 从节点升级为新主节点,此时新主节点上没有锁数据。
  4. 客户端 B 向新主节点请求锁,成功获取。

结果就是:同一把锁被两个客户端同时持有,分布式锁的互斥性被彻底破坏。

解决方案:Redis 官方提出了 RedLock 算法,即向多个独立的 Redis 节点(通常 5 个)依次请求锁,只有在大多数节点(N/2+1)上都获取成功,且总耗时小于锁的有效时间,才算加锁成功。但 RedLock 本身也充满争议,Martin Kleppmann 曾专门撰文质疑其安全性,认为它依赖时钟同步,在极端情况下依然不安全。

务实建议:如果业务对一致性要求极高,建议使用 ZooKeeper 或 etcd 实现分布式锁;如果使用 Redis,要接受它在极端情况下可能失效的事实,并做好兜底措施(比如数据库唯一约束)。

六、其他容易被忽略的坑

  • 锁的粒度过大:用一个大锁保护所有资源,导致并发度极低。应根据业务拆分锁的粒度。
  • 未设置超时时间:客户端获取锁时没有设置连接超时和读写超时,导致线程长时间阻塞。
  • 锁释放失败:释放锁时 Redis 连接断开,导致锁无法释放。需要结合过期时间兜底。
  • 时钟漂移:Redis 的过期时间依赖系统时钟,如果时钟发生跳变,可能导致锁提前或延迟过期。

总结

SETNX 实现分布式锁,看似简单,实则每一步都有坑:

解决方案
原子性 使用 SET key value NX EX
锁误删 value 存唯一标识 + Lua 脚本删除
过期时间 看门狗续期机制
可重入 使用 Redisson 等框架
主从切换 RedLock 或换用 ZooKeeper/etcd
锁粒度 按业务拆分锁

面试中回答这个问题,如果能把这些坑逐一讲清楚,并给出对应的解决方案和适用场景,基本就能让面试官眼前一亮。记住一句话:分布式锁没有银弹,理解其局限性比会用它更重要。

未经允许不得转载:任鹏个人博客 » Redis 的 SETNX 实现分布式锁有哪些坑

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏