ZooKeeper 实现分布式锁
一、分布式锁的典型使用场景
在分布式系统中,多个节点可能并发访问同一份共享资源。为保证数据一致性与操作原子性,需引入分布式锁机制。典型场景包括:
- 共享资源互斥访问
如多服务实例同时更新数据库同一条记录、操作 Redis 同一个 Key、修改 HDFS 同一文件等。 - 分布式任务调度防重
定时任务(如 XXL-JOB、Quartz 集群)避免多个节点重复执行同一任务;爬虫集群避免重复抓取相同 URL。 - 集群资源抢占与协调
主节点选举、分布式队列消费顺序控制、全局限流计数器等。 - 跨服务事务协调
在最终一致性事务中(如订单创建 + 库存扣减),通过分布式锁确保关键步骤串行执行。
核心目标:在无中心协调者的情况下,实现跨进程、跨机器的互斥控制。
二、分布式锁的核心要求
一个可靠的分布式锁应满足以下特性:
| 特性 | 说明 |
|---|---|
| 互斥性 | 同一时刻仅一个客户端可持有锁 |
| 高可用性 | 锁服务本身不能成为单点故障 |
| 自动释放 | 客户端崩溃或网络断开后,锁能自动释放(防死锁) |
| 公平性(可选) | 按请求顺序获取锁,避免饥饿 |
| 可重入性(可选) | 同一客户端可多次获取同一把锁 |
| 高性能 & 低延迟 | 加锁/释放操作应高效,避免成为系统瓶颈 |
三、为什么选择 ZooKeeper?
ZooKeeper 凭借其强一致性和丰富的节点模型,天然适合作为分布式锁的底层基础设施:
| ZooKeeper 特性 | 对分布式锁的支持 |
|---|---|
| ZNode 路径全局唯一 | 天然支持互斥(创建已存在节点失败) |
| 临时节点(EPHEMERAL) | 会话断开自动删除,实现锁自动释放 |
| 临时顺序节点(EPHEMERAL_SEQUENTIAL) | 生成全局递增序号,支持公平排队 |
| Watcher 事件监听 | 锁释放时实时通知等待者,无需轮询 |
| ZAB 协议 + 集群部署 | 强一致性 + 高可用,无单点风险 |
注意:ZooKeeper 的写吞吐较低(通常 < 10k QPS),适用于低频、强一致性场景,不适用于高频短锁(如每秒万次加锁)。
四、ZooKeeper 分布式锁的两种实现方式
(一)基于普通临时节点的非公平锁(简单但有缺陷)
原理
所有客户端尝试创建同一个临时节点(如 /lock),成功者获得锁,失败者监听该节点的删除事件。
流程
# [Client-A] 创建临时节点 → 成功,获得锁
create -e /lock "A"
# [Client-B] 创建失败 → 监听 /lock 删除事件
get -w /lock
# [Client-A] 业务完成 → 删除节点
delete /lock
# [Client-B] 收到事件 → 重新尝试创建抢锁
create -e /lock "B"
缺陷:惊群效应(Herd Effect)
- 锁释放时,所有等待客户端同时被唤醒并竞争,造成:
- ZooKeeper 瞬间高负载
- 大量无效请求(仅 1 个成功)
- 获取顺序不可控(不公平)
仅适用于低并发、测试或简单场景,生产环境不推荐。
(二)基于临时顺序节点的公平锁(生产首选)
原理
- 所有客户端在锁目录(如
/locks)下创建临时顺序节点(如/locks/lock-0000000001); - 客户端检查自己是否是序号最小的节点:
- 是 → 获得锁;
- 否 → 监听前一个序号的节点;
- 当前驱节点被删除(释放锁),当前客户端被通知,重新判断是否最小。
流程示例
# 初始化锁目录(持久节点)
create /locks ""
# [Client-A] 创建临时顺序节点 → /locks/lock-0000000001
create -s -e /locks/lock- ""
# [Client-B] 创建 → /locks/lock-0000000002
create -s -e /locks/lock- ""
# [Client-B] 获取子节点列表:[lock-0000000001, lock-0000000002]
# 发现自己不是最小 → 监听 lock-0000000001
get -w /locks/lock-0000000001
# [Client-A] 释放锁 → 删除自己的节点
delete /locks/lock-0000000001
# [Client-B] 收到事件 → 重新获取子节点 → [lock-0000000002] → 获得锁
流程图
graph TD
A["Client"] --> B["在 /locks 下创建临时顺序节点"]
B --> C["获取所有子节点并排序"]
C --> D{"自己是最小节点?"}
D -- 是 --> E["获得锁,执行业务"]
D -- 否 --> F["监听前一个序号的节点"]
E --> G["业务完成,删除自身节点"]
F --> H["前驱节点被删除,收到事件"]
H --> C
G --> I["后继节点被通知,重新判断"]
核心优势
- 公平性:先请求者先获得锁
- 无惊群效应:每次仅唤醒一个等待者
- 自动释放:临时节点随会话销毁
- 高可用:基于 ZK 集群,无单点
(三)两种方案对比总结
| 维度 | 普通临时节点(非公平) | 临时顺序节点(公平) |
|---|---|---|
| 公平性 | ❌ 不保证 | 严格 FIFO |
| 惊群效应 | 严重 | 无 |
| 实现复杂度 | 简单 | 中等 |
| 适用场景 | 低并发、Demo | 生产环境、高可靠系统 |
| 通知范围 | 广播(所有等待者) | 单播(仅下一个) |
| 推荐程度 | ⭐ | ⭐⭐⭐⭐⭐ |
结论:生产环境应优先采用基于临时顺序节点的公平锁实现。
五、工程实践建议
5.1 使用 Curator Framework(强烈推荐)
Apache Curator 封装了 ZooKeeper 分布式锁的复杂逻辑,提供开箱即用的 InterProcessMutex:
// 初始化 Curator 客户端
CuratorFramework client = CuratorFrameworkFactory.newClient("zk1:2181,zk2:2181",
new ExponentialBackoffRetry(1000, 3));
client.start();
// 创建可重入公平锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/order-service");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
// 执行临界区业务(如扣库存)
processOrder();
}
} finally {
lock.release(); // 自动处理异常、会话恢复、Watcher 重注册
}
InterProcessMutex默认实现的就是基于临时顺序节点的可重入公平锁,且处理了 Watcher 一次性、连接中断等边界情况。
5.2 最佳实践
- 锁路径命名规范:按业务隔离,如
/locks/payment-service、/locks/user-profile - 合理设置会话超时:默认 60s,需 > 最大业务执行时间,避免锁提前释放
- 异常必须释放锁:使用
try-finally或 Curator 自动管理 - 避免长业务持有锁:锁内只做关键操作,耗时逻辑移出临界区
5.3 常见陷阱与规避
| 陷阱 | 风险 | 规避方案 |
|---|---|---|
| Watcher 是一次性的 | 事件触发后未重注册,导致后续变化无法感知 | 使用 Curator,或手动在事件回调中重新注册 |
| 会话超时 < 业务时间 | 锁被自动释放,其他节点进入临界区 | 评估业务耗时,调大会话超时(如 2~5 分钟) |
| 网络闪断导致假死 | 客户端未真正宕机,但 ZK 认为其离线 | 启用 Curator 的重连策略 + 会话恢复机制 |
| 高频加锁 | ZK 写性能成为瓶颈 | 改用 Redis(RedLock)或本地锁 + 分布式锁分层 |
六、与其他方案的对比(扩展思考)
| 方案 | 一致性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| ZooKeeper | 强一致(CP) | 中低( | 中(需处理 Watcher) | 主节点选举、配置管理、低频强一致锁 |
| Redis(RedLock) | 最终一致(AP) | 极高(10w+ QPS) | 高(需处理时钟漂移) | 高频短锁、缓存更新、限流 |
| Etcd | 强一致(Raft) | 中高 | 中 | Kubernetes 场景、云原生应用 |
选型建议:
- 要强一致 + 自动释放 → 选 ZooKeeper
- 要高性能 + 可接受短暂不一致 → 选 Redis
七、总结
- ZooKeeper 实现分布式锁的核心在于:临时顺序节点 + Watcher 监听前驱;
- 公平锁(顺序节点)是生产环境唯一推荐方案,有效避免惊群效应;
- 不要直接使用原生 ZK API,优先采用 Curator Framework 等成熟封装;
- 分布式锁不是“银弹”,需结合业务频率、一致性要求、性能指标综合选型;
- 锁只是手段,减少锁竞争、缩小临界区、异步化才是高并发系统的根本解法。