如何用 Zookeeper 实现分布式锁?
面试考察点
- 方案演进思维:面试官不仅仅是想知道最终方案,更是想看你是否理解 "简单互斥锁 → 公平锁 → 可重入锁" 这个演进过程,以及每一步解决了什么问题。
- ZooKeeper 特性运用:临时节点、顺序节点、Watch 机制这三大特性如何在锁方案中组合使用,你能否讲清每个特性解决的是什么问题?
- 工程实践意识:生产环境中的锁超时、可重入、羊群效应、Curator 框架的使用这些你是否了解?
核心答案
ZooKeeper 实现分布式锁有三种方案,从简到繁逐步演进:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 方案一:普通临时节点 | 创建同一个临时节点,成功即获锁 | 简单直接 | 羊群效应,惊群问题 |
| 方案二:临时顺序节点 | 创建临时顺序节点,序号最小者获锁 | 公平锁,避免惊群 | 实现较复杂 |
| 方案三:Curator 封装 | 直接用 InterProcessMutex | 可重入、生产可用 | 需引入框架 |
生产环境直接用方案三(Curator 框架),面试重点讲方案二的原理。
深度解析
一、方案一:普通临时节点(不推荐)
最朴素的思路:所有客户端争抢创建同一个临时节点,比如 /lock,谁创建成功谁就获得锁。
上图展示了方案一的加锁流程。核心逻辑很简单:尝试创建 /lock 临时节点,成功了就获得锁,失败了就 Watch 等。
致命问题:羊群效应(Herd Effect)
当锁释放(/lock 节点被删除)时,所有等待的客户端都会收到 Watch 通知,然后同时涌上来争抢创建节点。假设有 1000 个客户端在等锁,锁一释放就是 1000 个并发写请求砸向 ZooKeeper,而最终只有一个能成功,其余 999 个白跑一趟。
这就是 "惊群" 问题——大量客户端被无效唤醒,给 ZooKeeper 造成巨大压力,严重时甚至会导致集群性能抖动。
二、方案二:临时顺序节点(推荐理解原理)
这是 ZooKeeper 分布式锁的 标准实现方案,解决了羊群效应,实现了公平锁。
上图展示了方案二的完整加锁流程。和方案一相比,核心改进在于:
- 每个客户端创建自己的顺序节点,不再是争抢同一个节点。ZooKeeper 保证顺序节点的序号是递增的,所以先来的客户端序号一定更小。
- 序号最小的客户端获得锁,不需要争抢。
- 每个客户端只 Watch 前一个节点,而不是 Watch
/lock父节点。这样当锁释放时,只有下一个排队者被通知,其余客户端完全不受影响——彻底解决了羊群效应。 - 天然公平:先来先得,按创建节点的顺序排队。
释放锁的流程分两种情况:
- 正常释放:客户端完成业务后,主动删除自己创建的临时顺序节点。
- 异常释放:客户端宕机或会话超时,临时节点自动删除。下一个等待的客户端收到 Watch 通知,判断自己为最小序号后获得锁。这就是临时节点的价值——保证锁一定会被释放,不会死锁。
三、方案三:Curator 框架封装(生产推荐)
方案二的原理搞清楚后,生产环境直接用 Apache Curator 封装好的 InterProcessMutex,不用自己手写。
// 创建 CuratorFramework 客户端
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
// 创建分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/order-lock");
try {
// 加锁(支持超时)
if (lock.acquire(3, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
processOrder();
} finally {
// 释放锁
lock.release();
}
}
} catch (Exception e) {
// 加锁超时或异常处理
log.error("获取分布式锁失败", e);
}
InterProcessMutex 的优势:
| 特性 | 说明 |
|---|---|
| 可重入 | 同一个线程可以多次获取同一把锁,内部用 ConcurrentHashMap 记录重入次数 |
| 超时机制 | 支持 acquire(timeout) 带超时的加锁,避免无限等待 |
| 锁自动释放 | 基于临时节点,客户端宕机后锁自动释放 |
| 连接恢复 | 内部处理了 ZooKeeper 连接断开重连后锁状态的恢复 |
四、三种常见锁的对比
面试官可能会让你比较 ZooKeeper 锁和 Redis 锁:
选型建议一句话:金融级、锁可靠性要求高的场景选 ZooKeeper;高并发、性能优先的场景选 Redis;没有中间件的小项目用数据库锁凑合。
面试高频追问
-
ZooKeeper 分布式锁和 Redis 分布式锁有什么区别?怎么选?
核心区别在一致性模型。ZooKeeper 是 CP,写操作需要半数以上节点确认,所以锁的可靠性高,不会出现 "两个人同时拿到锁" 的情况。Redis 是 AP,主从异步复制,主节点宕机时可能锁信息还没同步到从节点,导致锁丢失——这就是 RedLock 试图解决的问题。
不过 Redis 的性能确实比 ZooKeeper 高很多,大部分互联网场景用 Redis 锁就够了。金融交易这种对锁可靠性要求极高的场景才需要 ZooKeeper。
-
ZooKeeper 的临时节点在什么情况下会被误删?
网络抖动导致心跳超时,会话失效,临时节点被删除。但此时客户端其实还活着,只是跟 ZooKeeper 的网络不通了。这就是 "假死" 问题——客户端以为锁还在,实际上锁已经被 ZooKeeper 删了,其他客户端已经拿到了新锁。
解决方案:给锁加一个版本号(利用 ZNode 的
version字段),释放锁时检查版本号是否匹配,不匹配说明锁已经被别人拿了。 -
Curator 的
InterProcessMutex是怎么实现可重入的?内部维护了一个
ThreadData对象,用ConcurrentHashMap<Thread, LockData>存储每个线程的锁计数。同一线程第一次加锁计数为 1,每重入一次 +1,每释放一次 -1,减到 0 才真正删除节点释放锁。
常见面试变体
- "ZooKeeper 分布式锁和 Redis 分布式锁的区别?"
- "ZooKeeper 分布式锁的羊群效应是什么?怎么解决?"
- "如何保证分布式锁的可靠性?"
- "Curator 的
InterProcessMutex实现原理是什么?"
记忆口诀
三种方案演进:争一个节点(惊群)→ 排队等前一个(公平)→ 用框架(省心)。
方案二核心:创建顺序节点排队 → 序号最小获锁 → Watch 前一个 → 释放通知下一个。
为什么用临时节点:人走了自动释放,不死锁。
总结
ZooKeeper 分布式锁的核心是方案二(临时顺序节点 + Watch 前驱节点),实现了公平锁并解决了羊群效应。面试时重点讲清三点:为什么从方案一演进到方案二(解决惊群)、为什么用临时节点(异常自动释放,不死锁)、为什么只 Watch 前一个节点(避免羊群效应)。最后提一句 Curator 的 InterProcessMutex 可重入实现,加分项。
