ZooKeeper 实现分布式锁的两种方式

17 阅读7分钟

ZooKeeper 实现分布式锁

一、分布式锁的典型使用场景

在分布式系统中,多个节点可能并发访问同一份共享资源。为保证数据一致性与操作原子性,需引入分布式锁机制。典型场景包括:

  1. 共享资源互斥访问
    如多服务实例同时更新数据库同一条记录、操作 Redis 同一个 Key、修改 HDFS 同一文件等。
  2. 分布式任务调度防重
    定时任务(如 XXL-JOB、Quartz 集群)避免多个节点重复执行同一任务;爬虫集群避免重复抓取相同 URL。
  3. 集群资源抢占与协调
    主节点选举、分布式队列消费顺序控制、全局限流计数器等。
  4. 跨服务事务协调
    在最终一致性事务中(如订单创建 + 库存扣减),通过分布式锁确保关键步骤串行执行。

核心目标:在无中心协调者的情况下,实现跨进程、跨机器的互斥控制


二、分布式锁的核心要求

一个可靠的分布式锁应满足以下特性:

特性说明
互斥性同一时刻仅一个客户端可持有锁
高可用性锁服务本身不能成为单点故障
自动释放客户端崩溃或网络断开后,锁能自动释放(防死锁)
公平性(可选)按请求顺序获取锁,避免饥饿
可重入性(可选)同一客户端可多次获取同一把锁
高性能 & 低延迟加锁/释放操作应高效,避免成为系统瓶颈

三、为什么选择 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 个成功)
    • 获取顺序不可控(不公平)

仅适用于低并发、测试或简单场景,生产环境不推荐。


(二)基于临时顺序节点的公平锁(生产首选)

原理
  1. 所有客户端在锁目录(如 /locks)下创建临时顺序节点(如 /locks/lock-0000000001);
  2. 客户端检查自己是否是序号最小的节点
    • 是 → 获得锁;
    • 否 → 监听前一个序号的节点
  3. 当前驱节点被删除(释放锁),当前客户端被通知,重新判断是否最小。
流程示例
# 初始化锁目录(持久节点)
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)中低(1k10k QPS)中(需处理 Watcher)主节点选举、配置管理、低频强一致锁
Redis(RedLock)最终一致(AP)极高(10w+ QPS)高(需处理时钟漂移)高频短锁、缓存更新、限流
Etcd强一致(Raft)中高Kubernetes 场景、云原生应用

选型建议

  • 强一致 + 自动释放 → 选 ZooKeeper
  • 高性能 + 可接受短暂不一致 → 选 Redis

七、总结

  1. ZooKeeper 实现分布式锁的核心在于:临时顺序节点 + Watcher 监听前驱
  2. 公平锁(顺序节点)是生产环境唯一推荐方案,有效避免惊群效应;
  3. 不要直接使用原生 ZK API,优先采用 Curator Framework 等成熟封装;
  4. 分布式锁不是“银弹”,需结合业务频率、一致性要求、性能指标综合选型;
  5. 锁只是手段,减少锁竞争、缩小临界区、异步化才是高并发系统的根本解法。