题目
设计基于ZooKeeper的分布式锁服务并解决惊群效应和羊群效应
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
分布式锁实现原理,惊群效应解决方案,羊群效应优化,Watch机制深度使用,高并发场景设计
快速回答
实现分布式锁的核心步骤:
- 使用
create()创建临时顺序节点作为锁请求 - 获取父节点下所有子节点并排序
- 若当前节点是最小序号节点则获得锁
- 否则监听前一个节点的删除事件
- 锁释放时删除自身节点
解决惊群/羊群效应:
- 避免所有客户端监听同一节点
- 采用顺序监听策略(每个客户端只监听前驱节点)
- 设置合理的重试退避机制
一、核心原理说明
分布式锁实现:利用ZooKeeper的EPHEMERAL_SEQUENTIAL节点特性:
- 临时节点:客户端断开自动删除,避免死锁
- 顺序节点:天然实现锁的公平排队
- Watch机制:实现阻塞等待通知
二、惊群效应与羊群效应问题
惊群效应:当锁释放时,大量等待客户端同时被唤醒竞争资源,导致网络风暴。
羊群效应:所有客户端监听同一个节点,当该节点变化时ZooKeeper需向所有客户端发送通知,造成服务端压力。
三、解决方案与代码示例
// 伪代码实现(Curator框架简化版)
public boolean tryLock() {
// 1. 创建临时顺序节点
ourPath = zk.create("/lock/lock-", EPHEMERAL_SEQUENTIAL);
// 2. 获取所有子节点并排序
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);
// 3. 判断是否获得锁
if (ourPath.equals("/lock/" + children.get(0))) {
return true; // 获得锁
}
// 4. 监听前驱节点(关键解决惊群效应)
int ourIndex = children.indexOf(ourPath.substring(6));
String watchPath = "/lock/" + children.get(ourIndex - 1);
zk.exists(watchPath, watcher); // 仅监听前一个节点
// 5. 阻塞等待前驱节点删除事件
wait();
return true;
}四、最佳实践
- 监听策略:每个客户端只监听直接前驱节点,避免全局通知
- 重试机制:采用指数退避重试(如:100ms, 200ms, 400ms...)
- 锁释放:必须用
try-finally确保删除节点 - 会话超时:设置合理的sessionTimeout(建议10-30s)
五、常见错误
| 错误类型 | 后果 | 解决方案 |
|---|---|---|
| 未处理连接丢失 | 产生僵尸锁 | 使用临时节点 + 重连校验 |
| 未排序子节点 | 锁分配不公平 | 按序号排序(如:lock-000001) |
| 未监听前驱节点 | 羊群效应 | 严格按顺序监听 |
六、扩展知识
- 锁类型:共享锁(读锁)可通过不同节点前缀实现
- 性能优化:当超过1000并发时,建议分片锁路径(如:/shard_lock/01/)
- 替代方案:对比Redis分布式锁的优劣(CP vs AP)
- Curator框架:推荐使用InterProcessMutex,已内置解决方案