5.2 判例二:分布式锁的排号与叫号


文档摘要

5.2 判例二:分布式锁的排号与叫号 本节摘要:分布式锁用临时顺序节点实现:抢锁变成排号,序号最小者持锁,其余只监听自己的前序节点;持锁会话一死节点即删,锁自动放行。本判例先手写一版迷你锁看清全貌,再对照 Curator 的 InterProcessMutex 理解工业实现的补强点。 背景:三台机器抢一个批处理位 某结算服务三实例部署,每天凌晨都要跑一次对账批处理,任务要求全局只能有一个实例在跑——跑两次就重复入账。JVM 内的 synchronized 管不了跨进程,数据库锁又引入新的瓶颈点。需要的是一把"任何机器都承认、持锁者死了自动归还"的锁。把第四章的机制组合起来:顺序性(4.1 的 zxid 排序思想落到节点名上)负责公平排号,临时性(4.

5.2 判例二:分布式锁的排号与叫号

本节摘要:分布式锁用临时顺序节点实现:抢锁变成排号,序号最小者持锁,其余只监听自己的前序节点;持锁会话一死节点即删,锁自动放行。本判例先手写一版迷你锁看清全貌,再对照 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 后五秒内(会话超时内)锁节点消失。

工业版:InterProcessMutex 补了什么

生产直接用 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 已内置这套逻辑,判例四的队列场景不展开,思路与上面完全同源:把并发意图编码进节点名,让排序规则做裁决

要点回顾

  • 锁的三件套:临时保证持锁者死锁必放,顺序保证公平排号,前序监听保证叫号只惊动一人。
  • 只监听前序节点:这是分布式锁实现里最重要的防羊群设计,监听全体是新手实现的通病。
  • InterProcessMutex 的补强:可重入、超时、异常闭环、连接状态处理——迷你锁缺的它都有。
  • 临界区必须短于租约:否则执行中被过户,两人同跑;解法是放宽会话或切小任务。

常见疑问

问:为什么不用"创建同一个节点,谁成功谁持锁"这种更简单的实现? 它少两样东西:公平性(后来者永远在重试,先来者可能饿死)与性能(每次释放都是全员惊动)。临时顺序节点多一次 create 的开销,换来排队秩序与安静的等待队列,这笔账划算。

下一个判例从"互斥"转向"领导":业务集群怎么选出唯一的工作主节点。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U