题目
设计一个基于ZooKeeper的分布式锁并解决羊群效应
信息
- 类型:问答
- 难度:⭐⭐
考点
ZooKeeper临时顺序节点, 分布式锁原理, 锁的公平性与羊群效应
快速回答
实现分布式锁的核心步骤:
- 在锁节点(如
/lock)下创建临时顺序节点 - 获取锁节点下所有子节点并排序
- 若当前节点是序号最小的节点,则获取锁
- 若非最小节点,则:
- 监听前一个序号节点的删除事件
- 收到通知后重新检查序号
- 释放锁时删除自身节点
解决羊群效应的关键:每个节点只监听前一个节点,避免所有节点监听同一个节点。
解析
一、分布式锁原理
ZooKeeper通过临时顺序节点实现分布式锁:
- 临时节点:客户端断开连接时自动删除,避免死锁
- 顺序节点:ZooKeeper自动追加全局唯一序号(如
lock-00000001) - 监听机制:通过Watcher监听节点变化实现阻塞唤醒
二、核心代码示例(Java)
// 创建锁节点
String lockPath = zk.create("/lock/lock-",
null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取所有子节点并排序
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);
// 提取当前节点序号
String currentNode = lockPath.substring("/lock/".length());
int currentIndex = children.indexOf(currentNode);
if (currentIndex == 0) {
// 当前是最小节点,获得锁
return true;
} else {
// 监听前一个节点
String prevNode = children.get(currentIndex - 1);
CountDownLatch latch = new CountDownLatch(1);
Stat stat = zk.exists("/lock/" + prevNode,
event -> {
if (event.getType() == EventType.NodeDeleted) {
latch.countDown();
}
});
if (stat != null) {
latch.await(); // 阻塞等待前一个节点释放
}
return true; // 重新检查后获得锁
}三、羊群效应与解决方案
问题描述:若所有节点都监听最小节点,当锁释放时,所有客户端被唤醒并竞争资源,导致网络风暴。
解决方案:
- 每个节点只监听前一个序号节点
- 只有前一个节点释放时,当前节点才被唤醒
- 时间复杂度从O(n)降至O(1)
四、最佳实践
- 锁路径设计:使用明确前缀(如
/app/locks/order_lock) - 重试机制:添加超时和重试次数限制
- 锁释放:确保finally块中删除节点
- 会话超时:合理设置sessionTimeout(建议3-30秒)
五、常见错误
- 错误1:未处理连接断开——使用重试机制和临时节点
- 错误2:未排序节点——必须按序号排序
- 错误3:未监听前节点——导致羊群效应
- 错误4:未处理节点不存在——检查exists()返回值
六、扩展知识
- Curator框架:推荐使用其
InterProcessMutex实现(已解决上述问题) - 锁类型:读写锁(
InterProcessReadWriteLock) - ZooKeeper特性:顺序一致性保证锁公平性
- 对比其他方案:Redis锁(AP,无公平性)vs ZooKeeper锁(CP,强一致性)