本文目录导读:

ZooKeeper 利用临时顺序节点可以实现一套高效且公平的分布式锁(通常称为 “排他锁” 或 “写锁” )。
核心思想是:每个客户端尝试加锁时,在锁路径下创建一个临时顺序节点,通过比较自己创建的节点序号是否为当前路径下最小的,来判断是否获得锁,如果没获得锁,则监听前一个节点的删除事件。
以下是基于临时顺序节点实现分布式锁的完整原理、流程和代码示例。
核心概念回顾
- 临时节点 (Ephemeral):客户端会话断开后,节点自动删除,有效防止死锁(客户端崩溃锁自动释放)。
- 顺序节点 (Sequential):ZooKeeper 自动在节点名后追加一个全局唯一且递增的序号(如
lock-0000000001)。 - Watch 机制:客户端可以监听节点的变化(如删除、数据变化)。
实现原理与流程
假设锁的根节点为 /locks。
-
创建锁节点:每个想获取锁的客户端,在
/locks下创建临时顺序节点。- 客户端 A 创建
/locks/lock-0000000001 - 客户端 B 创建
/locks/lock-0000000002 - 客户端 C 创建
/locks/lock-0000000003
- 客户端 A 创建
-
判断获取锁:客户端获取
/locks下的所有子节点列表,并按序号排序。- 如果自己创建的节点是列表中序号最小的节点,则成功获取锁。
- 如果自己不是最小的,说明锁已被其他客户端持有。
-
等待与监听:
- 如果未获取到锁,客户端不会盲目轮询,而是监听比自己节点序号小的最后一个节点(即前一个节点)。
- 客户端 B 监听
/locks/lock-0000000001;客户端 C 监听/locks/lock-0000000002。 - 这种机制形成了一个等待队列,保证了公平性(FIFO,先进先出)。
-
释放锁:
- 正常释放:客户端处理完业务逻辑后,主动删除自己创建的临时节点。
- 异常释放:客户端会话过期或宕机,ZooKeeper 自动删除该临时节点。
-
唤醒后续节点:
- 客户端 A 释放锁(删除
/locks/lock-0000000001)。 - 客户端 B 监听到 A 节点被删除的事件,重新执行步骤 2,检查自己是否为最小序号节点,此时它是最小的,因此成功获取锁。
- 客户端 A 释放锁(删除
代码示例 (Java + Apache Curator)
在实际开发中,强烈建议使用 Curator 框架,它已经将上述流程封装成优雅的 InterProcessMutex,并处理了各种边界情况(如羊群效应、惊群效应等)。
虽然问题要求实现原生逻辑,但出于实用性,这里提供 Curator 的使用方式:
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class ZkLockExample {
private static final String ZK_ADDRESS = "127.0.0.1:2181";
private static final String LOCK_PATH = "/locks/my_lock";
public static void main(String[] args) throws Exception {
// 1. 创建 Curator 客户端
CuratorFramework client = CuratorFrameworkFactory.newClient(
ZK_ADDRESS,
new ExponentialBackoffRetry(1000, 3)
);
client.start();
// 2. 创建分布式锁(本质就是临时顺序节点)
InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);
try {
// 3. 获取锁(支持超时)
if (lock.acquire(10, TimeUnit.SECONDS)) {
System.out.println("获取锁成功,执行业务逻辑...");
// 模拟业务操作
Thread.sleep(5000);
} else {
System.out.println("获取锁失败,超时!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 4. 释放锁
if (lock.isAcquiredInThisProcess()) {
lock.release();
System.out.println("释放锁成功");
}
}
// 关闭客户端
client.close();
}
}
核心原理对比:临时顺序节点 vs 临时节点
| 特性 | 临时顺序节点 (推荐) | 临时节点 (不推荐) |
|---|---|---|
| 公平性 | 公平(FIFO),按请求顺序排队 | 不公平,抢锁顺序不确定 |
| 羊群效应 | 低,只监听前一个节点(链式监听) | 高,所有等待者监听同一个锁节点,释放时惊群 |
| 性能 | 高(O(n) 仅监听一个节点) | 低(O(N^2) 所有节点同时被唤醒) |
| 实现复杂度 | 稍复杂(需要排序和链式监听) | 简单,但性能差 |
| 死锁处理 | 自动删除(临时 + 会话超时) | 自动删除(临时 + 会话超时) |
优缺点分析
优点:
- 公平锁:严格按照请求顺序获取锁,避免了线程“饥饿”问题。
- 避免羊群效应:每个客户端只监听前一个节点,锁释放时只有一个客户端被唤醒,而不是所有客户端都去抢锁。
- 没有死锁:利用临时节点特性,客户端崩溃会自动释放锁,服务端无需额外处理。
- 高可用:ZooKeeper 集群保证锁服务的高可用。
缺点:
- 性能瓶颈:每次锁操作都需要与 ZooKeeper 集群进行多次网络通信(创建节点、获取子节点列表、设置监听),在高并发场景下,ZooKeeper 可能成为瓶颈(吞吐量通常低于 Redis)。
- 需考虑会话超时:如果业务执行时间过长,超过 ZooKeeper 会话超时时间,锁会被自动释放,导致其他客户端获取到锁(可能产生并发问题),需要权衡业务耗时和会话超时时间。
- 实现复杂度:相比 Redis 的 Redlock 或简单的 SETNX,手动实现原生 API 稍复杂(Curator 已解决此问题)。
应用场景
- 需要严格公平锁的场景:如任务调度、全局 ID 生成、资源顺序访问。
- 对锁可靠性要求极高的场景:如金融交易、库存扣减、关键配置更新。
- 对高吞吐量要求不极端,但对一致性要求高的场景。
使用 ZooKeeper 临时顺序节点实现分布式锁:
- 核心机制:创建临时顺序节点 + 监听前一个节点。
- 最大优势:公平、避免羊群效应、自动释放(安全)。
- 工业推荐:直接使用 Apache Curator 的
InterProcessMutex,生产验证、简单可靠。 - 注意事项:合理设置会话超时时间,避免业务未完成锁被提前释放。