分布式锁是分布式系统的基础原语:多个实例竞争同一资源时,需要一个「跨进程的互斥机制」——单机的 synchronized 或 mutex 只能锁进程内的线程,跨进程需要外部协调者。三条主流路线——Redis 的 SET NX、ZooKeeper 的临时顺序节点、数据库的唯一索引——各有性格,本篇讲清它们的实现与取舍。

路线一:Redis 的 SET NX——最流行的选择
Redis 分布式锁的核心命令:SET key value NX PX 30000——NX(Not eXists)保证只有 key 不存在时才设置(互斥性),PX 30000 设置 30 秒过期(防死锁——持锁方崩溃后锁自动释放)。释放锁必须用 Lua 脚本(保证「判断是否是自己的锁 + 删除」的原子性——否则可能误删别人的锁):
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Redis 锁的三个隐患。隐患一:锁过期但业务未完成——持锁方执行了 35 秒,锁 30 秒后过期被他人获取——解法是看门狗(watchdog)机制(后台线程定期续期,Redisson 的默认实现),但这引入了「看门狗线程本身崩溃怎么办」的新问题。隐患二:主从切换导致锁丢失——主节点写入锁后宕机,从库晋升为新主但尚未同步到这把锁,另一个客户端在新主上获取了同一把锁——Redis 的主从复制是异步的,无法保证锁在故障转移时不丢失。RedLock 算法(向多个独立 Redis 实例同时获取锁,超过半数成功才算成功)试图解决这个问题,但其正确性在分布式系统学界存在争议(Martin Kleppmann 与 antirez 的著名论战)。隐患三:没有公平性——多个客户端竞争时,锁的获取顺序不确定(可能饥饿)。
路线二:ZooKeeper 的临时顺序节点——最安全的方案
ZooKeeper 分布式锁的实现:每个客户端在锁节点下创建临时顺序节点(如 /lock/seq-00000001),然后检查自己是否是序号最小的子节点——是则获取锁,否则监听前一个节点的删除事件(避免羊群效应——不是监听整个锁节点)。ZooKeeper 锁的优势:临时节点保证会话断开时锁自动释放(客户端崩溃 → 会话超时 → 节点删除 → 锁释放——比 Redis 的过期时间更可靠,因为 ZooKeeper 的会话超时是集群 consensus 的结果,不是本地的 TTL);顺序节点保证公平性——按序号排队,不会饥饿;Watch 机制的推送模型——前一个节点释放时,下一个客户端立即收到通知,不需要轮询。劣势:ZooKeeper 的写入性能远低于 Redis(每次写操作需要集群过半确认),且 ZooKeeper 本身的运维复杂度高于 Redis。
路线三:数据库的唯一索引——最简单的兜底
数据库分布式锁的实现:创建一张锁表(lock_name 加唯一索引),获取锁就是 INSERT 一行(唯一索引冲突 = 获取失败),释放锁就是 DELETE 该行。优势:实现极简(不需要额外的基础设施——数据库通常已经有了);劣势:性能最差(每次获取/释放都有数据库 IO),且数据库宕机 = 锁服务不可用。适用场景:低频的分布式锁需求(如每日定时任务的互斥——每天只执行一次,数据库锁完全够用)。
选型框架:三条路的决策树
选择的标准取决于三个因素:锁的竞争频率(高频选 Redis,低频选数据库);安全性的要求(锁丢失不可接受选 ZooKeeper,可接受选 Redis);基础设施的现状(已有 Redis 选 Redis,已有 ZooKeeper 选 ZooKeeper)。多数场景的现实答案是 Redis + 看门狗——性能好、生态成熟(Redisson),虽然理论上不是最安全的,但对绝大多数业务场景足够安全。需要最高安全性的场景(金融、支付),选 ZooKeeper 或 etcd(类似的 consensus-based 锁)。一句话总结:Redis 的锁是「快的锁」,ZooKeeper 的锁是「稳的锁」——快与稳的取舍,是分布式系统永恒的主题。
数据库分布式锁的变体:乐观锁方式
数据库分布式锁除了 INSERT 方式,还有一种乐观锁的变体:使用 UPDATE 语句的原子性实现锁。获取锁:UPDATE lock_table SET owner = 'instance_1', version = version + 1 WHERE lock_name = 'resource_x' AND owner IS NULL——UPDATE 成功(affected rows = 1)则获取锁。释放锁:UPDATE lock_table SET owner = NULL WHERE lock_name = 'resource_x' AND owner = 'instance_1'。乐观锁方式的优势:不需要额外的 INSERT/DELETE 权限(某些受限数据库用户只有 UPDATE 权限);劣势:需要定期清理过期的 owner(持有方崩溃后 owner 永远非 NULL——需要超时清理机制)。数据库锁的终极建议:低频够用、高频不选、生产环境优先 Redis 或 ZooKeeper。
看门狗机制:Redisson 的续期实现
Redis 分布式锁的过期但业务未完成问题有一个工业级解法:Redisson 的看门狗机制。当使用 Redisson 的 lock 方法(不指定 leaseTime)获取锁时,Redisson 自动启动一个后台线程(看门狗),每 10 秒检查一次:如果锁仍被当前线程持有,自动续期(将过期时间重置为 30 秒)——只要业务代码还在执行,锁就不会过期。业务执行完毕后看门狗停止续期、锁释放。看门狗的优雅之处:它将过期时间的设定从开发者的责任转移给了框架——开发者不需要猜测业务需要多长时间。看门狗的风险:如果业务代码陷入死循环(线程永远不结束),看门狗会永远续期——锁永远不会释放导致死锁。因此看门狗不是万能药,业务代码仍需有超时保护。
分布式锁的使用场景:什么时候真的需要
分布式锁不是万能的——先确认你是否真的需要它。真正的分布式锁场景:定时任务的互斥(多个实例部署时确保定时任务只执行一次——但可以考虑「任务分片」替代锁);库存的防止超卖(高并发下单时防止库存被扣为负——但更好的方案是数据库乐观锁或 Redis 的 DECR 原子操作);缓存的防止击穿(热点 key 过期时只有一个请求去 DB 加载——但更好的方案是逻辑过期或互斥锁的细粒度版本)。不需要分布式锁的场景:数据库的唯一索引已经保证了唯一性(不需要应用层再加锁);消息队列的消费已经保证了单消费者(MQ 的分区消费天然互斥)。分布式锁的最佳实践是「能用数据库的原子操作就不用分布式锁,能用地域分片就不用全局锁」——锁是最后手段,不是默认方案。
Redis 锁的 RedLock 争议:Kleppmann vs antirez
RedLock 算法的正确性争议是分布式系统领域最著名的技术辩论之一。RedLock 的流程:向 N 个独立的 Redis 实例同时发送 SET NX 请求,超过半数成功且总耗时小于锁的有效期则获取锁成功。Martin Kleppmann(《DDIA》作者)的批评:RedLock 的安全性依赖时钟的准确性——如果某个 Redis 实例的时钟跳变(NTP 校时),锁的过期时间判断就会出错,可能导致两个客户端同时持有锁。antirez(Redis 作者)的反驳:RedLock 的随机起始点设计降低了时钟跳变的影响,且系统的时钟管理应确保 NTP 配置为逐步校时(step 模式才可能导致跳变)。Martin 的进一步回应:即使时钟不跳变,GC 暂停(STW)也可能导致客户端在「获取锁成功后」长时间暂停,醒来时锁已过期——与时钟跳变等效。这场辩论的实际启示:Redis 锁在「足够好」和「绝对安全」之间存在灰色地带——对于资金安全等极端场景,建议使用 ZooKeeper/etcd 或数据库锁;对于一般场景,Redis + 看门狗足够。
RedLock 争议:Kleppmann 与 antirez 的辩论
RedLock 算法的正确性争议是分布式系统领域最著名的技术辩论之一。RedLock 的流程:向 N 个独立的 Redis 实例同时发送 SET NX 请求,超过半数成功且总耗时小于锁的有效期则获取锁成功。Martin Kleppmann 的批评:RedLock 的安全性依赖时钟的准确性——如果某个 Redis 实例的时钟跳变,锁的过期时间判断就会出错。antirez 的反驳:RedLock 的随机起始点设计降低了时钟跳变的影响。Martin 的进一步回应:即使时钟不跳变,GC 暂停也可能导致客户端在获取锁成功后长时间暂停,醒来时锁已过期。这场辩论的实际启示:Redis 锁在足够好和绝对安全之间存在灰色地带——对于资金安全等极端场景建议使用 ZooKeeper 或 etcd,对于一般场景 Redis 加看门狗足够。
分布式锁的使用场景与 RedLock 争议
分布式锁不是万能的——先确认是否真的需要。真正需要的场景:定时任务的互斥、库存防止超卖、缓存防止击穿。RedLock 算法的正确性争议是分布式系统领域最著名的技术辩论之一。Martin Kleppmann 的批评是 RedLock 的安全性依赖时钟的准确性。antirez 的反驳是 RedLock 的随机起始点设计降低了时钟跳变的影响。这场辩论的实际启示是 Redis 锁在足够好和绝对安全之间存在灰色地带——对于资金安全等极端场景建议使用 ZooKeeper 或 etcd。
小结
分布式锁收成四条:Redis 的 SET NX + Lua 脚本是最流行的方案——但有过期但业务未完成、主从切换锁丢失、不公平性三个隐患;ZooKeeper 的临时顺序节点最安全——临时节点防死锁、顺序节点保公平、Watch 推送避免轮询,但写入性能远低于 Redis;数据库的唯一索引是最简的兜底——低频场景够用,高频场景不可行;选型的决策树——竞争频率决定 Redis vs 数据库、安全性要求决定 Redis vs ZooKeeper。分布式锁的最佳实践:无论选哪条路,都要在代码里加超时兜底与日志——分布式锁的故障排查,日志是你唯一的救命稻草。
参考资料
- Martin Kleppmann, How to do distributed locking —— RedLock 正确性争议的经典文章
- 本库前篇:《ZooKeeper 一致性协议 ZAB 原理》《缓存一致性实战》
- Redisson 官方文档 —— Redis 分布式锁的工业级实现
读者留言
COMMENTS 暂无还没有留言,来说第一句?