如何用 ZooKeeper 实现分布式锁?为什么用临时顺序节点?
简化版
ZooKeeper 实现分布式锁靠临时顺序节点(EPHEMERAL_SEQUENTIAL):所有客户端在同一个父节点下创建临时顺序子节点,ZK 会给它们编号(递增)。序号最小的那个客户端获得锁;其他客户端不去反复轮询,而是只 watch(监听)自己前一个节点,前一个被删除(锁释放)时才被唤醒去尝试。用临时节点是因为客户端断连/宕机时 ZK 会自动删除它的节点(防死锁),用顺序节点是为了实现公平锁 + 只监听前驱避免惊群。
详细版
加锁流程:
- 客户端在锁的父节点(如
/lock)下创建一个临时顺序节点,如/lock/req-0000000001。 - 获取父节点下所有子节点,排序,判断自己的序号是不是最小的。
- 是最小 → 获得锁,执行业务。
- 不是最小 → 找到比自己小一位的那个节点(前驱),对它注册 watch 监听,然后阻塞等待。
- 当前驱节点被删除(前一个客户端释放锁/宕机),watch 触发,客户端被唤醒,回到第 2 步重新判断(此时自己通常成了最小)。
解锁:删除自己创建的节点。临时节点在会话结束(正常解锁或宕机断连)时也会被 ZK 自动删除。
两个关键选型:
- 临时(Ephemeral):会话断开自动删除 → 持锁客户端宕机时锁自动释放,天然防死锁,不需要设过期时间。
- 顺序(Sequential):ZK 保证编号单调递增 → 天然形成排队顺序(公平锁),且每个节点只监听前一个,避免所有人监听同一节点造成的「惊群效应」。
完整版教学
一、为什么临时节点能防死锁
Redis 锁靠「过期时间」防死锁,但过期时间不好设(短了误删、长了等太久)。ZooKeeper 用临时节点优雅解决:临时节点的生命周期绑定客户端的会话(Session),客户端和 ZK 之间靠心跳维持会话。一旦客户端宕机/网络断开,心跳超时,会话失效,ZK 自动删除该客户端创建的所有临时节点——锁随之释放。不需要预估过期时间,也不会因为「业务超时」误删(会话没断锁就一直在),比 Redis 的过期机制更自然。
二、为什么用顺序节点 + 只监听前驱
如果所有客户端都创建普通节点、都去监听「锁节点」是否释放,那么锁一释放,所有等待者同时被唤醒去抢,只有一个成功,其余又继续等——这就是「惊群效应(herd effect)」,大量无效唤醒和竞争,浪费资源。
ZK 用顺序节点把等待者排成一队(按编号),每个客户端只监听紧挨在自己前面的那一个节点。这样锁释放时(最小节点删除),只有排在它后面的下一个被唤醒,其余人纹丝不动。既实现了公平(按创建顺序 FIFO 获得锁),又消除了惊群(每次只唤醒一个)。这是 ZK 分布式锁设计的精髓。
记忆点:临时节点 = 防死锁(宕机自动释放);顺序节点 + 只 watch 前驱 = 公平锁 + 防惊群。两个特性结合,才是 ZK 锁优于 Redis 锁的地方。
三、ZK 锁为什么可靠(CP 特性)
ZooKeeper 是 CP 系统,基于 ZAB 协议,写操作要过半节点确认才成功。所以「创建锁节点」这个写操作一旦返回成功,就意味着已经被多数派持久化,不会像 Redis 主从那样因异步复制 + 主宕机而丢锁。这让 ZK 锁在一致性上强于 Redis 锁,适合对正确性要求高的场景。代价是:每次加解锁要走共识(过半确认),性能比 Redis 低(Redis 是内存单点操作,ZK 要多节点协调)。
四、ZK 锁的不足
- 性能较低:加解锁涉及节点创建/删除 + watch,且要过半确认,QPS 不如 Redis。
- 羊群问题需正确实现:如果实现时偷懒监听父节点而非前驱,仍会惊群。
- 会话超时的误判:如果客户端因长时间 GC 或网络抖动导致心跳超时,ZK 会认为它挂了、删除临时节点、释放锁,但客户端其实还活着——和 Redlock 的 GC 问题类似,ZK 也不能完全避免(但发生概率和影响可控)。
- 依赖 ZK 集群:要额外维护 ZK 集群。
五、Redis 锁 vs ZooKeeper 锁(速记)
| 维度 | Redis 锁 | ZooKeeper 锁 |
|---|---|---|
| CAP | AP(性能优先) | CP(一致性优先) |
| 防死锁 | 过期时间(要续期) | 临时节点(会话断自动删) |
| 公平性 | 默认非公平 | 顺序节点天然公平 |
| 性能 | 高(内存单点) | 较低(共识+watch) |
| 可靠性 | 主从可能丢锁 | 过半确认不易丢锁 |
| 适用 | 高并发、容忍极小概率丢锁 | 强一致、正确性优先 |
六、常见误区与追问
这道题不能只背概念,要把「ZooKeeper 分布式锁」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | ZooKeeper 锁常用临时顺序节点,序号最小者获得锁,前驱节点删除后下一个节点被唤醒 | 不要停在名词解释 |
| 流程机制 | 创建临时顺序节点 -> 获取同目录节点排序 -> 序号最小获得锁 -> 否则监听前一个节点 -> 前驱删除后重新判断 -> 会话断开临时节点自动删除 | 说明触发方、参与方、状态变化和兜底 |
| 工程取舍 | 客户端创建 /lock/seq-00000003,若前面有 seq-00000002,就只 watch 这个前驱节点 | 分布式锁只能控制临界区进入,不能替代业务幂等和状态校验 |
ZooKeeper 分布式锁 面试拆解:
1. 创建临时顺序节点
2. 获取同目录节点排序
3. 序号最小获得锁
4. 否则监听前一个节点
5. 前驱删除后重新判断
6. 会话断开临时节点自动删除
记忆钩子:先说为什么单机锁失效,再说明加锁、过期、续期、解锁、防误删和故障恢复;回答时要紧扣「ZooKeeper 分布式锁」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:所有等待者都监听同一个节点。 应监听前驱节点,避免锁释放时惊群。
- 误区:ZooKeeper 锁不需要释放。 正常完成要删除节点,异常断开才靠临时节点自动释放。
- 误区:ZooKeeper 锁性能一定比 Redis 高。 ZooKeeper 更强一致,但写入和 watch 成本通常高于 Redis。
- 追问:为什么用临时节点? 客户端会话失效时节点自动删除,避免死锁。
- 追问:为什么用顺序节点? 天然形成排队顺序,避免所有客户端抢同一把锁。
- 追问:ZooKeeper 锁适合什么? 强一致协调、选主、低频关键互斥场景。
七、加强记忆
ZooKeeper 分布式锁:所有客户端在父节点下建临时顺序节点,序号最小者持锁,其余只监听前一个节点、前驱删除时才被唤醒。临时节点让客户端宕机时会话失效、节点自动删除,天然防死锁(不用设过期);顺序节点 + 只 watch 前驱实现公平锁并消除惊群。ZK 是 CP 系统,写过半确认,锁不易丢失,可靠性强于 Redis 主从锁,但性能较低。高并发用 Redis,强正确性用 ZK。