题目
如何基于ZooKeeper实现一个公平的分布式锁?请描述核心流程并解决惊群效应问题
信息
- 类型:问答
- 难度:⭐⭐
考点
ZooKeeper临时顺序节点,分布式锁设计,惊群效应处理,Watch机制应用
快速回答
实现公平分布式锁的核心步骤:
- 在锁节点下创建临时顺序节点作为锁请求
- 获取父节点下所有子节点并排序
- 若当前节点是最小序号节点则获得锁
- 若非最小节点,则向前一个节点注册Watcher
- 前序节点释放锁时触发回调重新检查
解决惊群效应:通过定向监听前序节点而非父节点,避免所有等待节点同时被唤醒。
解析
一、核心原理
ZooKeeper通过临时顺序节点(EPHEMERAL_SEQUENTIAL)实现分布式锁:
- 临时性:客户端断开连接时节点自动删除,避免死锁
- 顺序性:节点按创建顺序编号(如lock-000001),实现公平排队
二、完整实现流程
// 1. 创建锁节点(持久节点,若不存在)
zk.create("/distributed_lock", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 2. 当前客户端创建临时顺序节点
String myNode = zk.create("/distributed_lock/lock-",
null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 3. 获取所有子节点并排序
List<String> children = zk.getChildren("/distributed_lock", false);
Collections.sort(children);
// 4. 判断当前节点是否为最小节点
String currentNode = myNode.substring("/distributed_lock/".length());
int myIndex = children.indexOf(currentNode);
if (myIndex == 0) {
// 获得锁
return true;
} else {
// 5. 监听前一个节点(解决惊群效应关键)
String prevNode = children.get(myIndex - 1);
final CountDownLatch latch = new CountDownLatch(1);
Stat stat = zk.exists("/distributed_lock/" + prevNode,
event -> {
if (event.getType() == EventType.NodeDeleted) {
latch.countDown();
}
});
if (stat != null) {
latch.await(); // 阻塞直到前序节点释放
}
// 重新检查锁状态
return checkLockStatus();
}三、惊群效应解决方案
问题描述:若所有等待线程都监听父节点,当锁释放时所有线程被唤醒,导致网络风暴。
解决方案:每个线程只监听自己前序节点:
- 释放锁时仅触发后一个节点的Watcher
- 唤醒次数从O(N)降低到O(1)
四、最佳实践
- 锁重入:在节点数据中存储客户端ID和重入计数
- 超时机制:使用
CountDownLatch.await(timeout)避免永久阻塞 - 连接恢复:Session过期时需重新申请锁
- 锁释放:finally块中确保删除节点
五、常见错误
- 错误1:未处理节点已存在的异常(
KeeperException.NodeExistsException) - 错误2:未校验Watcher触发时的节点状态(可能因连接断开导致状态不一致)
- 错误3:未考虑网络延迟导致的时序问题(使用zxid辅助判断)
六、扩展知识
- 对比Redis分布式锁:ZooKeeper通过Watcher机制避免轮询,更适用于长持有锁场景
- Curator框架:Apache Curator提供的
InterProcessMutex已实现优化锁逻辑 - 锁升级:读写锁可通过不同前缀节点实现(如
read_/write_)