5.2 判例二:分布式锁的排号与叫号 本节摘要:分布式锁用临时顺序节点实现:抢锁变成排号,序号最小者持锁,其余只监听自己的前序节点;持锁会话一死节点即删,锁自动放行。本判例先手写一版迷你锁看清全貌,再对照 Curator 的 InterProcessMutex 理解工业实现的补强点。 背景:三台机器抢一个批处理位 某结算服务三实例部署,每天凌晨都要跑一次对账批处理,任务要求全局只能有一个实例在跑——跑两次就重复入账。JVM 内的 synchronized 管不了跨进程,数据库锁又引入新的瓶颈点。需要的是一把"任何机器都承认、持锁者死了自动归还"的锁。把第四章的机制组合起来:顺序性(4.1 的 zxid 排序思想落到节点名上)负责公平排号,临时性(4.
本节摘要:分布式锁用临时顺序节点实现:抢锁变成排号,序号最小者持锁,其余只监听自己的前序节点;持锁会话一死节点即删,锁自动放行。本判例先手写一版迷你锁看清全貌,再对照 Curator 的 InterProcessMutex 理解工业实现的补强点。
某结算服务三实例部署,每天凌晨都要跑一次对账批处理,任务要求全局只能有一个实例在跑——跑两次就重复入账。JVM 内的 synchronized 管不了跨进程,数据库锁又引入新的瓶颈点。需要的是一把"任何机器都承认、持锁者死了自动归还"的锁。把第四章的机制组合起来:顺序性(4.1 的 zxid 排序思想落到节点名上)负责公平排号,临时性(4.4 的会话租约)负责持锁者死后自动放行,Watcher(4.3)负责叫号。
理解锁的最佳路径是自己写一遍。核心动作三步:建锁目录、创建临时顺序节点、判断自己是否序号最小。
public class MiniMutex { private final CuratorFramework client; private final String lockPath = "/locks/settle-job"; private String myNode; // 我排的号 public MiniMutex(CuratorFramework client) { this.client = client; } public void acquire() throws Exception { // 1. 排号:临时 + 顺序,服务端自动追加十位编号 myNode = client.create().creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(lockPath + "/lock-", "holder".getBytes()); while (true) { List<String> kids = client.getChildren().forPath(lockPath); Collections.sort(kids); // 序号最小即队首 if ((lockPath + "/" + kids.get(0)).equals(myNode)) { return; // 我就是队首,锁到手 } // 2. 找到我的前序节点,只监听它一个 —— 防羊群的关键 String prev = kids.get(kids.indexOf(myNode.split("/")[3]) - 1); CountDownLatch latch = new CountDownLatch(1); client.getData().usingWatcher(evt -> latch.countDown()) .forPath(lockPath + "/" + prev); latch.await(); // 3. 睡到前序节点被删除(前一个持锁者放行) } } public void release() throws Exception { client.delete().guaranteed().forPath(myNode); // 删号即放行,guaranteed 确保删成 } }

对照运行验证:四个终端各起一个实例抢锁,日志应严格按序号依次出现"acquired",且释放后下一个立即接位。用 dump 四字命令能看到锁节点与会话的归属关系,持锁实例 kill -9 后五秒内(会话超时内)锁节点消失。
生产直接用 Curator 的实现,它比迷你锁多补四件事:可重入(同一线程可反复 acquire,本地的锁表按线程记账)、超时获取(不无限等)、异常闭环(acquire 与 release 配对校验)、连接状态处理(SUSPENDED 时暂停判锁、LOST 时确认锁已失效)。用法与迷你锁同构:
InterProcessMutex lock = new InterProcessMutex(client, "/locks/settle-job"); if (lock.acquire(30, TimeUnit.SECONDS)) { // 最多等 30 秒,防集群异常时无限挂起 try { runSettlement(); // 临界区:全局唯一执行 } finally { lock.release(); // 必须配对释放,finally 兜底 } } else { alertOps("本次未抢到锁,跳过执行"); // 拿不到锁是正常分支,要有业务兜底 }
两条使用铁律。临界区时间与租约对表:持锁执行 90 秒而会话超时 30 秒,执行到一半锁已过户给下家,两人同跑——要么把 sessionTimeout 放宽到大于最大临界区(4.4 节的原则),要么把临界区切小。可重入别跨线程:InterProcessMutex 的重入记账是线程级的,线程 A 的锁线程 B 不能解,这是刻意设计。
排它锁对读密集场景过于严苛——十台机器同时读配置本可并行。共享锁(读写锁)的排号表加一列"意图标记":写者排 write- 序号,读者排 read- 序号,规则改为"写者前面有任何未清的号都得等;读者前面只有别的读者时可进"。Curator 的 InterProcessReadWriteLock 已内置这套逻辑,判例四的队列场景不展开,思路与上面完全同源:把并发意图编码进节点名,让排序规则做裁决。
问:为什么不用"创建同一个节点,谁成功谁持锁"这种更简单的实现? 它少两样东西:公平性(后来者永远在重试,先来者可能饿死)与性能(每次释放都是全员惊动)。临时顺序节点多一次 create 的开销,换来排队秩序与安静的等待队列,这笔账划算。
下一个判例从"互斥"转向"领导":业务集群怎么选出唯一的工作主节点。