后端如何设计分布式锁才能避免死锁和误删
分布式锁避坑的核心只有三条:加锁时原子地带上过期时间、删除锁时校验持有者标识、临界区外再用 fencing token(单调递增的令牌)兜底。这三条做到,死锁和误删基本不会出现。
分布式锁为什么会死锁?
结论:死锁几乎都来自"加锁"和"设过期时间"两步不原子,或者拿到锁的进程崩溃后没人释放。
典型错误写法是先 SETNX lock 1,成功后再 EXPIRE lock 30。这两条命令之间只要客户端被 kill、容器被调度走或网络断开,锁就永久留在 Redis 里,后续所有请求全部阻塞,只能人工删 key。正确写法是一条命令完成:SET lock_key unique_value NX PX 30000。NX 表示 key 不存在才写入,PX 30000 表示 30 秒后自动过期,Redis 保证两个语义在同一次命令中原子生效,从根上消灭"永不过期的锁"。
第二类问题不是死锁而是互斥失效:锁有 TTL,但临界区里等下游接口、跑大批量任务,耗时超过 TTL。此时锁已过期,另一个线程拿到锁,两个线程同时进入临界区。这比死锁更危险,因为它不报错。办法是把临界区压缩到只做必要操作,长任务拆成多段、每段单独加锁。
锁被误删怎么解决?
结论:只在"值等于自己写入的唯一标识"时才删锁,并且用 Lua 脚本把判断和删除做成原子操作。
误删的场景是:A 拿到锁,TTL 到期,B 拿到同一把锁;A 的业务跑完后执行 DEL lock_key,把 B 的锁删掉了,于是 C 也能加锁成功。修复方式是加锁时把 value 写成唯一标识,例如 UUID.randomUUID().toString() + ":" + Thread.currentThread().getId()。
释放锁用官方推荐的 Lua 脚本:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
不能先 GET 再 DEL,两条命令之间仍有窗口。Lua 脚本在 Redis 中是单线程执行,判断与删除之间不会插入其他命令。
锁到期了业务还没跑完怎么办?
结论:要么用带 Watchdog 的客户端自动续期,要么让锁退出正确性关键路径,改用 fencing token 兜底。
Redisson 的默认行为是:不显式指定 leaseTime 时,租约 lockWatchdogTimeout 默认为 30 秒,后台线程每 10 秒(租约的 1/3)续期一次;JVM 崩溃后续期停止,锁最多 30 秒后自动释放,不会死锁。但续期解决不了 stop-the-world 的 GC 停顿——线程被挂起超过 30 秒,锁照样过期。
工程上更稳的是 fencing token:加锁时让 Redis 用 INCR 返回一个单调递增序号,请求带着它去写下游存储,存储层拒绝序号小于已见最大值的写入。这样即使两个线程短暂并行,先拿到旧序号的请求也会被拒。Martin Kleppmann 正是用 GC 停顿和时钟漂移两点质疑 Redlock,而 Redlock 要求 5 个独立节点中至少 3 个加锁成功,运维成本明显更高;单实例 Redis 配 fencing token 更划算。
Redis 和 ZooKeeper 分布式锁有什么区别?
结论:ZooKeeper 靠临时节点自动释放,不存在"锁过期但业务还在跑"的问题;Redis 靠 TTL,吞吐更高但要自己处理续期和误删。
ZooKeeper 的做法是在 /lock 下创建临时顺序节点,序号最小的持有锁,其余节点只监听自己的前一个节点,避免惊群。客户端会话断开或进程崩溃后,临时节点自动删除,锁立即释放,不需要 TTL。代价是写操作要走 ZAB 协议多数派确认,吞吐低于 Redis。
选型上:并发高、允许极小概率互斥失效的场景(缓存重建、定时任务防重)用 Redis;对正确性要求高、并发不高的场景(选主、配置变更)用 ZooKeeper。数据库唯一索引也能当锁,INSERT INTO lock_table(name) VALUES('x') 插入成功即持锁,靠事务提交或连接断开释放,实现最简单但性能最差。
收尾
分布式锁不是"加锁删锁"两行代码,而是一套不变量:加锁用 SET key value NX PX 一条命令;value 带唯一标识;释放走 Lua 比较删除;长任务靠续期但别依赖续期;真正要求强正确性的地方,用 fencing token 让下游存储做最后一道拒绝。选型上再按并发量和对正确性的要求,在 Redis、ZooKeeper、数据库唯一索引之间做取舍。
原文链接:https://www.gj0.com/thread-162.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。