三种分布式锁方案对比
2023年双十一前夜,我们的支付团队做了一个"常规"的技术升级:将分布式锁从 ZooKeeper 切换到 Redis。
理由很充分:Redis 性能更好,团队更熟悉,运维更简单。
上线后 2 小时,订单系统出现了诡异的重复扣款:2000 多笔订单被重复支付,总金额超过 180 万元。
复盘结果:Redis 锁在主从切换时丢锁了——主节点宕机,从节点晋升,但从节点的数据落后了 30 秒。这 30 秒里,另一个客户端拿到了同一把锁。
这把锁,本该是保护用户钱袋子的。
今天这篇,我们把 Redis、ZooKeeper、etcd 三种主流分布式锁方案掰开揉碎讲清楚,帮你在这三个方案之间做出正确选择。
问题定义
分布式锁,本质上要解决的是多进程之间对共享资源的互斥访问问题。
在单机环境下,synchronized 或 ReentrantLock 就能搞定。但一旦涉及多节点部署,就需要跨进程的协调机制:
- 谁拿到了锁?
- 锁什么时候释放?
- 节点崩溃了怎么办?
- 网络分区了怎么办?
这三个问题,Redis、ZK、etcd 各自给出了不同的答案。
【架构权衡】
分布式锁没有银弹。Redis 快但有丢锁风险,ZK 一致性强但性能最低,etcd 性能居中但运维最复杂。选错方案的代价不仅是技术债务,更可能是真金白银的损失(如上文的 180 万)。所以选型之前,先问自己三个问题:对一致性要求有多高?团队能驾驭多复杂的方案?出问题了能否快速回滚?
方案一:Redis 分布式锁
核心原理
Redis 分布式锁基于 SETNX(SET if Not eXists)语义,通过 SET key value NX PX timeout 原子性地获取锁。
关键设计点:
- value 必须唯一:用 UUID 或客户端标识,防止误删他人的锁
- 必须设置过期时间:防止节点崩溃后锁永远不释放
- 释放锁必须用 Lua 脚本:保证 get + del 的原子性
锁续期: watchdog 机制
如果业务执行时间超过锁过期时间怎么办?Redisson 框架提供了 watchdog 自动续期:
每 10 秒自动把锁的 TTL 重置为 30 秒,只要业务还在执行,锁就不会丢。
【架构权衡】
Redis 锁的最大优势是性能。在单机 Redis 环境下,QPS 可以达到 10 万以上,远超 ZK 和 etcd。但代价是牺牲了强一致性。Redis 的主从复制是异步的,主节点宕机时从节点可能没有最新数据,这就是"主从切换丢锁"的根因。RedLock 通过多数节点共识来解决这个问题,但会带来延迟增加和运维复杂度上升。
方案二:ZooKeeper 分布式锁
核心原理
ZK 分布式锁基于临时顺序节点 + Watch 机制:
ZK 锁的核心逻辑:
- 每个想要获取锁的客户端在
/locks下创建临时顺序节点 - 判断自己是否是序号最小的节点:如果是,拿到锁;如果不是,监听前一个节点
- 当前一个节点被删除(客户端断开连接或业务完成),收到通知后重新争抢
ZK 锁的天然优势
- 自动释放:临时节点在客户端断开时自动删除,不怕节点崩溃
- 公平锁:按序号排队,先到先得
- 顺序保证:不需要额外的一致性机制
ZK 锁的劣势
ZK 使用 ZAB 协议保证一致性,每次锁操作都需要过半数节点确认,性能远低于 Redis。在高并发场景下,ZK 锁的 QPS 通常只能达到几千。
【架构权衡】
ZK 的设计哲学是"强一致、慢但稳"。ZK 的 ZAB 协议保证了线性一致性,但代价是每次操作都需要 Leader 节点协调,加上过半确认的延迟。另一个被忽视的问题是:ZK 集群的节点数必须是奇数(3/5/7),而且 ZK 不适合存储大量数据——如果你的锁数量超过 1 万,性能会明显下降。
方案三:etcd 分布式锁
核心原理
etcd 分布式锁基于其事务机制和租约(Lease)模型,与 ZK 有很多相似之处,但底层使用了 Raft 协议:
etcd 的锁利用了 MVCC 机制:每次修改都生成一个新的版本号,配合前缀查询和范围事务,可以实现公平的顺序锁。
etcd 的优势
- 强一致性:Raft 协议提供线性一致性,性能优于 ZK(单节点 QPS 可达数万)
- 租约机制:锁的过期和续期由租约管理,与业务逻辑分离
- 多版本并发控制:revision 机制天然支持公平锁和锁排队
- HTTP/gRPC 接口:比 ZK 的 ZKClient 更轻量
【架构权衡】
etcd 是这三者中最"年轻"的方案,但它继承了 ZK 的强一致性保证,同时在性能上更接近 Redis。etcd 的劣势在于运维复杂度:它需要单独部署集群,学习曲线比 Redis 高,出了问题排查难度也更大。另外 etcd 的官方客户端在 Java 生态不如 Redis 成熟,所以选 etcd 之前要确认团队是否有能力驾驭它。
完整对比矩阵
生产避坑
Redis 锁的五个致命坑
坑一:锁过期了但业务还没执行完
如果设置了 30 秒超时,但业务需要 60 秒,锁会自动释放,另一个客户端会拿到同一把锁。
解决方案:使用 watchdog 续期(Redisson)或将锁 TTL 设置为业务预估时间的 2-3 倍。
坑二:主从切换丢锁
主节点宕机后,从节点晋升但数据缺失,导致两个客户端同时拿到锁。
解决方案:使用 RedLock(5 个独立 Redis 节点)或改用 etcd/ZK。
坑三:锁被误删
客户端 A 获取锁后超时,锁被自动释放;客户端 B 拿到锁;客户端 A 执行完成后删除了客户端 B 的锁。
解决方案:释放锁时用 Lua 脚本判断 value 是否匹配。
坑四:集群模式下锁获取不对称
Redis Cluster 模式下,锁 key 可能在不同槽位,不同节点的 DEL 操作无法保证原子性。
解决方案:Redisson 的集群锁使用 hash slot 计算,保证同一 key 在同一节点。
坑五:CLH 队列饥饿问题
使用 ZK 或 etcd 的顺序锁时,如果持有锁的客户端崩溃,后续所有客户端都要等待。
解决方案:监控锁获取延迟,设置超时告警。
ZK 锁的坑
ZK 锁最大的坑是羊群效应(Herd Effect):锁释放时,所有等待的客户端都会收到通知,同时去争抢锁,造成惊群效应。
解决方案:使用临时顺序节点,只监听前一个节点而不是锁节点本身。
etcd 锁的坑
etcd 的 Lease 需要手动续期,如果忘记续期,锁会提前过期。
解决方案:使用 keepAlive 自动续期机制,或将 Lease TTL 设置为预估业务时间的最大值。
场景化推荐
【架构权衡】
没有最优方案,只有最适合业务场景的方案。以下推荐基于行业实践,但实际选型还需结合团队能力和运维成本综合判断。
工程代价评估
落地 Checklist
- 确认业务对一致性的要求(CP 还是 AP)
- 评估高峰 QPS,选择锁方案
- 实现 watchdog/续期机制,防止锁提前释放
- 释放锁使用 Lua 脚本,保证原子性
- 锁的 value 使用唯一标识(UUID),防止误删
- 设置锁获取超时,避免无限等待
- 添加锁获取和释放的监控告警
- 全链路压测,验证锁方案在高并发下的正确性
- 制定主从切换时的兜底方案(降级、告警、人工介入)
- 编写故障演练脚本,验证锁方案在节点崩溃时的行为