24.2 2PC 与恢复协议
一句话
2PC 用 “先投票、后决定” 把多个本地事务绑成一个:协调者问 “能提交吗”,全票通过才发 commit;它的代价是两轮日志 fsync + 一个阻塞窗口——协调者挂掉时,已投票的参与者只能等。
两个阶段
| 阶段 | 协调者 | 参与者 |
|---|---|---|
| ① Prepare(投票) | 发 prepare,记录 “开始” 日志 | 执行本地事务到 “可提交” 状态(持锁、写 undo/redo),回 yes/no |
| ② Commit/Abort | 收齐投票 → 写决定日志 → 广播 | 按决定提交或回滚,回 ack |
协调者 参与者 A / B
|-- prepare -------------->|
|<---- yes ----------------| (持锁,等决定)
| 写 commit 日志(fsync) |
|-- commit ---------------->|
|<---- ack ----------------|
故障点与恢复
| 故障点 | 后果 | 恢复方式 |
|---|---|---|
| 参与者 prepare 后挂 | 协调者超时 → 全局 abort | 参与者重启后查日志/询问协调者 |
| 协调者 prepare 后挂 | 参与者持锁阻塞,不知该提交还是回滚 | 等协调者恢复(读其日志);这是 2PC 的致命窗口 |
| 协调者发完 commit 后挂 | 部分参与者已提交 | 重试广播;参与者幂等 |
| 参与者 commit 后挂 | 重启后重放日志完成 | 日志保证幂等 |
优化手段
| 优化 | 作用 |
|---|---|
| 只读参与者不投票 | 减少一轮开销(read-only optimization) |
| presumed abort | 协调者日志缺失时默认回滚,缩短阻塞 |
| 协调者高可用(共识复制) | 消除 “协调者挂 = 永久阻塞” |
| 事务超时 + 补偿 | 长时间未决时走业务补偿 |
数字
| 项 | 量级 |
|---|---|
| 一次 2PC 的 RTT | 2 轮(prepare + commit) |
| 日志 fsync | 协调者 1–2 次、参与者各 1–2 次 |
| 典型延迟 | 同机房 5–20ms,跨机房显著上升 |
| 阻塞窗口 | 协调者故障时长(可能无限) |
3PC 试图用 “多一个阶段” 减少阻塞,见 24.3 3PC。
实测:故障注入下会不会 “部分提交”
本机 Python 3.14 模拟(3 个参与者,每个在 prepare 阶段写日志后才回 yes):
| 场景 | 协调者决定 | 参与者最终状态 | 消息数 |
|---|---|---|---|
| 无故障 | commit | committed × 3 | 7 |
| 协调者在收齐投票后崩溃 | blocked → abort(presumed abort) | prepared × 3(等恢复) | 3 |
| 任一参与者回 no | abort | aborted × 3 | 7 |
断言检查:任何场景下都没有出现 “一部分 committed、一部分 aborted”——这正是 2PC 保证的 “原子性”,代价是第二行那个 “全体卡在 prepared”。
# 关键:投票前必须落盘,否则崩溃后无法兑现承诺
def prepare(self):
self.log.append("prepared") # 先写日志
self.fsync() # 再回 yes
return True
反模式
| 反模式 | 后果 |
|---|---|
| 协调者是单点 | 它挂 = 参与者全部持锁等死 |
| 用 2PC 包裹长业务 | 锁持有久,吞吐崩塌 |
| 参与者不写日志就回 yes | 崩溃后无法恢复,出现 “部分提交” |
| 把 2PC 当跨服务默认方案 | 微服务下链路长、可用性差(见 24.9 事务模型选型) |
三条铁律
🔑 投票前必须落盘;否则崩溃后无法兑现承诺。
🔑 协调者必须高可用;单点协调者 = 无限阻塞。
🔑 2PC 适合 “短、少、强一致”;长链路用 Saga/消息表。
自检
Q1 为什么参与者 prepare 之后要 “持锁等待”?
▸ 答案:因为它已经向协调者承诺 “我能提交”,如果此时释放锁让别人改了数据,commit 时就无法兑现。所以它必须保持资源冻结,直到收到最终决定——这正是 2PC 阻塞与吞吐问题的根源。
Q2 协调者在发完 prepare 后崩溃,参与者会怎样?
▸ 答案:阻塞。参与者已投 yes 但不知道全局决定,只能等协调者恢复(读它的日志)或由外部干预。若协调者永久丢失,参与者只能靠 “超时 + 人工/补偿” 解套。这是 2PC 最著名的缺陷。
Q3 “presumed abort” 优化解决什么问题?
▸ 答案:它规定 “没看到 commit 日志就按 abort 处理”。这样协调者在 prepare 阶段崩溃时,参与者可以安全回滚,而不必无限等待。前提是协调者必须在发 commit 前把决定落盘,否则会误回滚。
Q4 2PC 能保证 “不丢不重” 吗?
▸ 答案:在所有节点都遵守日志协议的前提下,能保证 “要么全提交、要么全回滚”。但网络分区 + 协调者失联时,它会牺牲可用性换取一致性(阻塞)。要 “既不阻塞又一致”,需要共识算法复制协调者状态。
Q5 为什么微服务里很少直接用 2PC?
▸ 答案:因为链路长、参与者多、跨团队/跨库,任何一环不可用都会拖住全局;且 2PC 要求参与者支持 XA/事务协议,很多中间件不支持。工程上更常用 Saga、本地消息表、事务消息(见 24.9 事务模型选型)。
Q7 实测里 “协调者崩溃” 场景只有 3 条消息就停了,这算成功还是失败?
▸ 答案:算安全但不活:参与者都停在 prepared、持锁等待,系统没有产生不一致(安全),但无法继续(活性受损)。这正是 2PC 的取舍——它选择 “不一致更可怕,所以宁可阻塞”。工程上靠协调者高可用(共识复制)把这个窗口压到秒级。