题目
如何基于Zookeeper实现分布式锁?请说明原理和实现细节
信息
- 类型:问答
- 难度:⭐⭐
考点
Zookeeper分布式锁原理,临时顺序节点,Watch机制,羊群效应避免
快速回答
基于Zookeeper实现分布式锁的核心步骤:
- 在指定路径(如
/locks)下创建临时顺序节点 - 获取父节点下所有子节点,判断当前节点是否是最小序号节点
- 若是最小节点则获得锁;否则监听前一个节点的删除事件
- 业务处理完成后主动删除节点释放锁
关键优化:
- 使用临时节点避免客户端崩溃导致的死锁
- 顺序节点+Watch机制防止羊群效应
一、实现原理
Zookeeper通过两个核心特性实现分布式锁:
- 临时顺序节点(Ephemeral Sequential):
- 节点在客户端会话结束时自动删除,避免死锁
- 顺序编号保证锁请求的全局顺序性
- Watch机制:
- 客户端可监听特定节点的变更事件
- 当锁释放时(节点删除),通知等待的客户端
二、完整实现流程
// 伪代码示例
public void lock() {
// 1. 创建临时顺序节点
String lockPath = zk.create("/locks/resource_",
null,
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 2. 获取所有子节点并排序
List<String> children = zk.getChildren("/locks", false);
Collections.sort(children);
// 3. 判断当前节点是否是最小节点
String currentNode = lockPath.substring("/locks/".length());
int currentIndex = children.indexOf(currentNode);
if (currentIndex == 0) {
// 获得锁
return;
} else {
// 4. 监听前一个节点
String prevNode = children.get(currentIndex - 1);
CountDownLatch latch = new CountDownLatch(1);
zk.exists("/locks/" + prevNode, event -> {
if (event.getType() == EventType.NodeDeleted) {
latch.countDown();
}
});
// 5. 阻塞等待前一个节点释放
latch.await();
}
}
public void unlock() {
zk.delete(lockPath, -1); // 删除节点释放锁
}三、关键问题解决
| 问题 | 解决方案 |
|---|---|
| 羊群效应(Herd Effect) | 只监听前一个节点而非所有节点,避免所有客户端同时被唤醒 |
| 客户端崩溃 | 临时节点自动删除,不会导致死锁 |
| 网络闪断 | 会话超时机制(sessionTimeout)自动清理节点 |
四、最佳实践
- 锁释放时机:
- 必须在finally块中释放锁,确保异常时也能释放
- 避免在锁内进行阻塞操作,减少锁持有时间
- 重试策略:
- 实现指数退避(Exponential Backoff)重试
- 设置最大重试次数防止活锁
- 锁粒度:
- 使用不同路径区分不同资源的锁(如
/locks/DB_TableA)
- 使用不同路径区分不同资源的锁(如
五、常见错误
- 误用永久节点:客户端崩溃后锁无法释放
- 未处理连接丢失:
- 场景:获取锁后Zookeeper连接断开
- 后果:临时节点被删除导致锁意外释放
- 方案:使用
CuratorFramework等封装库处理连接状态
- Watch一次性触发:
- 事件触发后需重新注册Watch
- 解决方案:在
exists()回调中重新设置Watch
六、扩展知识
- Curator Recipes:Apache Curator提供的现成锁实现(如
InterProcessMutex) - 锁类型对比:
方案 优点 缺点 Zookeeper 强一致性、无死锁 性能较低(~10k QPS) Redis 高性能(~100k QPS) 可靠性依赖持久化策略 - 分布式锁应用场景:
- 全局配置更新
- 分布式任务调度
- 防止重复请求处理