20.2 集中式互斥 (协调者)
一句话
集中式互斥把 “谁能进临界区” 交给一个协调者裁决,简单到只有 “请求—授权—释放” 三步;代价是它同时是单点、也是 “进程暂停后仍以为自己持锁” 的源头——所以生产上用的一定是 租约 + fencing token。
| 锁服务与业务共用集群 | 锁抖动直接放大为业务故障 | | 不监控锁等待时长 | 死锁/长持锁只能靠用户投诉发现 |
三个角色与流程
| 角色 | 职责 | 挂掉会怎样 |
|---|---|---|
| 客户端 | 申请、续租、释放 | 普通故障,锁到期自动释放 |
| 协调者(锁服务) | 维护 “谁持有、到期时间” | 单点:不能加锁;多副本则需共识 |
| 存储/资源 | 被保护对象 | 需要校验 token 才能拒绝旧持有者 |
1. 客户端 → 协调者:acquire(lock, ttl=15s)
2. 协调者:若空闲 → 返回 token=42 + 租约到期时间
3. 客户端执行临界区;期间按 ttl/3 续租
4. 释放(或到期自动释放)
两个救命机制
| 机制 | 解决什么 | 不做的后果 |
|---|---|---|
| 租约(TTL) | 持有者崩溃后锁不会永久占用 | 死锁,需要人工介入 |
| fencing token | 持有者 “暂停(GC/网络)后恢复” 仍以为持锁 | 两个进程同时写,数据损坏 |
fencing token 是单调递增的编号:协调者每次授权 +1,存储侧只接受 “比上次更大” 的 token。旧持有者恢复后带着旧 token 写,被直接拒绝——这是分布式锁 “正确性” 的最后一道闸(见 48.3 分布式锁)。
数字
| 项 | 量级 |
|---|---|
| 加锁一次 | 1 个 RTT(同机房 < 1ms) |
| 租约 TTL | 10–30 秒(取决于临界区时长) |
| 续租间隔 | TTL/3 |
| 故障后锁自动释放 | ≤ TTL |
另外两种去中心方案见 20.4 令牌环互斥,三者的取舍见 20.5 三种算法对比 (消息量 / 公平性 / 可用性)。
代码:租约 + fencing 的最小实现
// 加锁:返回单调递增的 token
func Acquire(ctx context.Context, key string, ttl time.Duration) (token int64, err error) {
return store.CompareAndSwap(ctx, key, "", ttl, /*newToken=*/store.NextSeq())
}
// 受保护的写入:存储侧拒绝旧 token(fencing 的落点)
func Write(ctx context.Context, key string, token int64, data []byte) error {
last, _ := store.Get(ctx, "last_token:"+key)
if token < last {
return ErrStaleOwner // 旧持有者的迟到写,直接拒绝
}
return store.Put(ctx, key, data, token) // 写入时把 token 一并落库
}
反模式
| 反模式 | 后果 |
|---|---|
| 不设 TTL | 持有者崩溃 = 永久死锁 |
| 有 TTL 但无 fencing | GC 停顿 20 秒后,旧持有者与新持有者同时写 |
| 锁服务多副本但无共识 | 脑裂后两个协调者各自授权 |
| 用 “锁在” 代替 “权限校验” | 资源侧无 token 校验,锁只是君子协定 |
三条铁律
🔑 租约必须有 TTL;否则锁服务就是死锁制造机。
🔑 必须配 fencing token;时间上的 “互斥” 无法防止暂停进程的迟到写入。
🔑 协调者自身要高可用(共识复制),否则它成为全局单点。
自检
Q1 为什么 “有 TTL 就不会死锁” 这句话只对一半?
▸ 答案:TTL 保证最终释放,但在 “进程暂停又恢复” 的场景下会引入双持有:旧持有者以为自己还在锁内,实际锁已过期并被别人拿走。防双持有要靠 fencing token 或资源侧校验,而不是靠 TTL。
Q2 fencing token 为什么必须由协调者单调递增地发?
▸ 答案:因为存储侧需要 “只接受更新编号” 的简单规则来判定新 owner。若 token 可重用或不单调,旧持有者的迟到写就与新持有者无法区分。单调递增让 “谁更新” 变成一个可比较的数。
Q3 锁服务用 3 副本 Raft,是否就没有单点了?
▸ 答案:仍有延迟与可用性代价:多数派可用才能服务,网络分区时少数派无法授权(这是正确行为)。它消除的是 “单机故障”,不是 “网络分区下的不可用”。要降低影响,可缩短租约、做本地缓存 + 降级策略。
Q4 临界区很长(几分钟)时,租约怎么设计?
▸ 答案:不要 “一个长租约”,而是短租约 + 持续续租 + fencing:每次续租失败就主动放弃临界区(因为可能已被他人获取)。同时把长任务拆成幂等的小段,避免 “持锁做长任务”。
Q5 什么场景适合集中式互斥,什么场景不适合?
▸ 答案:适合 “低竞争、需要强互斥、能接受 1 RTT” 的场景(选主、定时任务去重、配置变更)。不适合 “高频短临界区”(每次 1 RTT 太贵,应用本地锁/原子操作)与 “跨地域”(RTT 与分区风险)。
Q7 为什么 fencing 的校验必须放在存储侧,不能放在客户端?
▸ 答案:因为 “旧持有者” 本身就是不可信的(它可能已暂停、也可能已被分区隔离)。只有被写的资源(数据库/存储)掌握 “最后接受的 token”,才能拒绝过期写入。客户端自查等于让过期持有者自己承认过期——不可靠。
关联
- 前置:20.1 互斥问题与临界资源 (安全性 / 活性)
- 并列:20.3 分布式互斥 (Ricart-Agrawala / 全票通过)、20.4 令牌环互斥
- 下游:48.3 分布式锁、21.11 领导者租约