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 的取舍——它选择 “不一致更可怕,所以宁可阻塞”。工程上靠协调者高可用(共识复制)把这个窗口压到秒级。

关联