用第一篇的三个指标验收
前三篇攒足了 Redis 锁的修补经验,现在拿第一篇立下的三个硬指标(互斥、防死锁、可辨识)去验收 ZooKeeper 方案。先看流程——抢锁 lock:card:8801,四步:
1. 在 /locks/card-8801/ 下创建「临时顺序节点」
→ 得到 /locks/card-8801/seq-0000000042(序号全局单调,并发不重号)
2. 列出父节点下全部子节点,按序号排序
3. 自己是最小 → 直接持锁,进临界区
4. 不是最小 → 只 Watch 序号比自己小一位的前驱节点,然后等待
前驱删除时收到通知 → 回到第 2 步再看一遍排位释放锁更简单:删掉自己的那个临时节点。四步走完,三个指标逐一过验。
互斥:最小序号只有一个
「排位第一」是 ZooKeeper 集群仲裁过的事实:顺序节点的序号由服务端分配(ZAB 过半提交),并发创建绝不重号;同一时刻父目录下序号最小的节点有且只有一个。持锁权 = 最小序号,这是集群级共识背书的互斥,不存在 Redis 那种「主从各说各话」的窗口。附带一条免检福利:公平。序号即到达顺序,先到先得,天然无饿死——Redis 篇里要额外付吞吐代价才买到的 FairLock,这里是出厂标配。
防死锁:会话蒸发,锁跟着蒸发
这是 ZK 方案对 Redis 方案最体面的一击。临时节点绑定会话(第 8 篇),持锁客户端崩溃 → 连接断 → 心跳停 → 会话过期 → 锁节点被服务端自动删除 → 下一位立刻接棒。全流程没有 TTL 参数,没有看门狗线程,没有「过期时间设多长」的赌博——持有人活着,锁就活着;持有人死了,锁十分钟内(sessionTimeout)自动善后。锁的生命周期与进程活性天然绑定,防死锁是协议级保证,不是参数级补救。
可辨识与防羊群:只盯前一个
删锁删的是「自己的节点」,别人的删不了也轮不到删——可辨识在节点归属上天然成立。流程里最精巧的一笔是第 4 步的 「只 Watch 前驱,不 Watch 全体」。反面教材:一百个等待者全都 Watch 锁目录本身,持锁者一释放,一百个通知同时炸响,一百个客户端同时扑回来抢位——这就是第 6 篇的惊群(羊群效应)在 ZK 上的重演。正面做法:每个人只盯自己前面的那一个,锁释放一次,恰好唤醒一个等待者,队伍像传送带一样一格一格向前走,事件量从 O(N) 降到 O(1)。
| 指标 | Redis 锁的实现代价 | ZK 锁的获得方式 |
|---|---|---|
| 互斥 | SET NX + Lua,但主从切换有窗口 | 最小序号,集群共识背书 |
| 防死锁 | TTL + 看门狗线程,参数赌命 | 临时节点绑会话,协议级保证 |
| 可辨识 | UUID value + Lua 校验删除 | 节点归属天然成立 |
| 公平 | FairLock 额外 list 排队,吞吐受损 | 序号排队,出厂标配 |
| 防惊群 | pub/sub 广播,唤醒全体 | 只 Watch 前驱,一次唤醒一人 |
生产别手写:用 Curator
流程看着清爽,手写全是暗礁:会话抖动后 Watch 没了要重挂、连接中断要重试、节点删失败要兜底。生产直接用 Curator(Netflix 出品的 ZK 客户端),可重入互斥锁十行搞定:
InterProcessMutex lock =
new InterProcessMutex(curatorClient, "/locks/card-8801");
if (lock.acquire(5, TimeUnit.SECONDS)) { // 最多等 5 秒
try {
// 临界区业务(可重入:同线程反复 acquire 计数+1)
} finally {
lock.release(); // 计数-1,减到 0 才真删节点
}
}它还把重入做成了节点内计数(同 Redisson 的 Hash 思路),把公平锁、读写锁、信号量、屏障一并提供。代价也要老实交代:ZK 锁的性能天花板远低于 Redis——每次写锁节点都要走一遍过半提交,QPS 量级是「千」不是「十万」,所以它适合锁粒度粗、竞争频次低的场景(选主、任务分配、配置仲裁),不适合秒杀扣减这种高频抢锁。下一篇盘点 ZK 锁自己的坑:会话误判、幽灵复活、性能天花板——裁判也有看走眼的时候。
评论 (0)