分布式2PC理论
目录
什么是分布式 2PC(Two-Phase Commit)
2PC 的工作原理
2PC 的优缺点
为什么 2PC 不完全可靠?
超时问题
协调者故障
什么是分布式 2PC(Two-Phase Commit)
定义
2PC 是一种原子提交协议,用于把一次“跨多个节点/资源管理器”的事务做成要么全部提交、要么全部回滚。
参与角色只有两个:
协调者(Coordinator / 事务管理器 TM):发起、收集投票并下达最终决定。
参与者(Participants / RM):各个数据库/服务,执行本地操作并在提交前表态
资源管理器(RM)是管理具体数据资源的组件,即使在同一节点上,不同类型的资源管理器也属于多个资源。常见的资源管理器包括:
数据库
消息队列
缓存
分布式文件系统
例如一个事务需要同时完成数据库更新订单状态和消息队列发送通知消息,这两个操作分别由数据库 RM和消息队列 RM管理,属于跨资源管理器的事务。
两阶段握手(简化时序)
应用││ 1. 执行业务写入(各节点先本地执行但不提交)▼
协调者 ──Prepare──► 参与者1/2/.../N (阶段一:投票/准备)◄─Yes/No─┆ ◄─Yes/No─┆├─若全部 Yes → ──Commit──► 全部参与者 (阶段二:提交)└─有 No/超时 → ──Rollback► 全部参与者 (阶段二:回滚)
关键点
参与者在 Prepare 成功时必须把提交所需日志落盘、加锁/冻结相关行,承诺“收到 Commit 就一定能提交、收到 Rollback 就一定能回滚”
2PC 只负责原子提交的协调,并不替代各节点的并发控制(MVCC/2PL)与隔离级别
为什么要Prepare 阶段落盘 + 加锁/冻结?
当参与者收到协调者的 Prepare 请求时,它要做两件关键事:
1. 写日志(落盘)
在数据库里,这叫 Prepare Log
含义是:我已经执行了本地事务的操作(写入/更新),但是先不提交。
写日志到磁盘是为了保证:即使机器宕机重启,能恢复我当时是准备提交的状态,避免数据丢失。
2. 锁定相关资源
比如要更新一行库存,把它加上锁,保证别的事务不能改。
因为你还没最终决定提交还是回滚,所以必须冻结住,避免数据被其他事务破坏。
3. 承诺的含义
如果后来收到 Commit 指令:一定能提交(因为日志+锁都在)
如果收到 Rollback:一定能回滚(因为我记录了未提交前的状态)
2PC 只负责原子提交协调,不替代并发控制?
2PC 解决的问题:
保证一个跨节点事务,要么所有人都提交,要么所有人都回滚。
(即原子性 + 一致性)但它不解决的问题:
多个事务并发时的数据冲突。
比如两个事务同时扣库存,一个要减 5,一个要减 10,该谁先执行?会不会导致库存负数?
这种属于并发控制的范畴,需要 MVCC(多版本并发控制)或 2PL(两阶段加锁)等机制。
在分布式事务中的地位
2PC 是最经典最通用的原子提交基线方案,XA 标准就是基于 2PC。MySQL/PostgreSQL/Oracle 等都提供(XA、Prepared Transactions)
它把跨库/跨服务的一致提交从业务里抽象到事务管理器(TM)层来做,业务方只关心成功/失败
现代系统里,2PC常作为强一致方案的起点,在需要更高可用/弹性的场景,会引入 3PC、TCC、Saga、Paxos/Raft+两段提交等替代或改进。
什么是原子提交基线方案?
在分布式系统里,最基础最常见最权威的解决原子提交的方法就是 2PC原子提交 = 所有节点的结果要么全成,要么全败
基线方案 = 社区/学术界/工业界最普遍认可的最低要求的做法
换句话说:
2PC 是跨节点事务一致性的起点和底线
如果连 2PC 都没实现,就不能保证强一致性
XA 标准是啥?
XA 标准由 X/Open 组织提出的一种分布式事务接口规范。
它规定了事务管理器(TM)和资源管理器(RM) 之间的接口。
内核就是基于 2PC 协议。
结构:
TM(Transaction Manager):负责全局事务的开始、提交、回滚。
RM(Resource Manager):数据库、消息队列等资源。
XA 定义了 TM 调用 RM 的接口,比如 xa_start, xa_prepare, xa_commit, xa_rollback
所以XA是2PC的工业标准化版本
MySQL、Oracle、DB2 都支持XA接口
XA、Prepared Transactions 是啥?
XA 事务就是遵循 XA 标准的分布式事务。应用调用 TM,TM 协调多个 RM(数据库),执行 2PC 流程。比如 Java EE 里的 JTA(Java Transaction API) 就是 XA 的实现。
Prepared Transactions(预提交事务)
是数据库对 2PC 的支持点。
在 Prepare 阶段,数据库执行事务但不提交,写入 Prepare 日志。
PostgreSQL 就有 PREPARE TRANSACTION 'xid'; 命令
它允许事务在待提交/待回滚的状态下存活,直到协调者发出最终决定
这就是为什么数据库里有 prepared state 这种中间状态既不是 committed,也不是 rolled back。它就是 2PC 的产物
与单机事务的区别
维度 | 单机事务(本地 DB) | 分布式事务 2PC |
---|---|---|
参与节点 | 1 个 | 多个 RM |
提交决定 | 本地直接 COMMIT/ROLLBACK | 由协调者收集投票后统一决定 |
故障模型 | 只考虑本机/本库 | 还要考虑网络分区、消息丢失、任意一方宕机 |
锁的持有 | 持有到本地提交 | 从 Prepare 开始到最终决议都要持有(更长,易阻塞) |
恢复 | 通过 WAL/REDO/UNDO | 还需要全局事务日志和决议重放 |
代价 | 一次落盘 | 两轮网络往返 + 多节点落盘,延迟与抖动更大 |
单机是我自己说了算
2PC 是所有人都说能交卷,我再统一交;有人说不行就全部撤回
2PC 的工作原理
2PC(Two-Phase Commit,两阶段提交)就是:
在一个跨多个节点的事务中,引入一个 协调者(Coordinator),它负责向所有参与者(Participants) 发号施令。整个协议分为两个阶段:
准备阶段(Prepare/Voting Phase)
提交阶段(Commit Phase)
核心思想:先投票,再决策。
参与者要么一致答能提交,要么有任何一方答不行。
协调者最后统一下达全部提交或全部回滚的命令。
1. 协调者与参与者的角色
协调者(Coordinator)
类似总指挥。
负责发起事务、收集各参与者反馈、做出最终决策。
有权决定全体提交或全体回滚。
参与者(Participants)
每个分布式节点,比如数据库分片、库存服务、支付服务。
接到指令时:
在准备阶段执行本地事务并锁定资源。
在提交阶段根据协调者的决定,真正提交或回滚。
2. 消息交互流程(投票 + 提交)
整个过程的时序图:
应用事务发起│▼
[协调者] ------------------- 发起 Prepare 请求 ------------------► [参与者1..N]◄-------------------- 返回 Yes/No --------------------││ 收集所有投票│├── 如果全部 Yes → 发送 Commit 请求│ └────────► [参与者1..N 提交事务并解锁]│└── 如果有 No/超时 → 发送 Rollback 请求└────────► [参与者1..N 回滚事务并解锁]
交互过程就是:
投票阶段:协调者问 → 参与者答。
提交阶段:协调者决定 → 参与者执行。
两个阶段的作用
阶段一:准备阶段(Voting Phase / Prepare Phase)
协调者动作:
给所有参与者发出 Prepare 请求。
参与者动作:
执行本地事务操作
写入 Undo Log/Redo Log,并持久化
给相关记录加锁,防止并发修改
回复协调者Yes(我已准备好,可以提交)或 No(执行失败,不能提交)
作用:让所有参与者表态,保证在进入下一步前,大家都能提交
阶段二:提交阶段(Commit Phase)
协调者动作:
收集所有参与者的投票结果
如果全是 Yes → 发 Commit 请求
如果有 No 或超时 → 发 Rollback 请求
参与者动作:
收到 Commit:真正提交事务(把日志刷盘 + 更新数据可见 + 解锁)
收到 Rollback:撤销事务(按 Undo Log 回滚 + 解锁)
作用:做出最终决定,保证所有节点的事务命运一致
2PC 的优缺点
优点(为什么很多系统以 2PC 为起点)
1) 强一致性(原子提交)
保证跨多个资源的要么都成功,要么都失败。
发生在同一事务内的多处写入,由协调者统一裁决,避免部分提交的脏状态。
2) 模型简单、生态成熟
角色清晰(协调者/参与者)、时序固定(Prepare→Commit/Rollback),实现成本低。
XA 标准等接口广泛可用,落地门槛低。
2PC 是把跨节点的一组写操作变成不可分割的原子动作的通用基线方案
缺点(为什么实际大规模场景常“绕开”或“增强”2PC)
痛点 | 现象 | 成因 | 影响 | 常见缓解 |
---|---|---|---|---|
阻塞 | 参与者长时间持锁等待 | Prepare 后到最终决议之间必须锁定行/范围 | 吞吐下降、延迟陡增、易死锁 | 缩短事务,把耗时 IO 移到事务外;原子语句更新;乐观并发+重试;按键/分片限流 |
协调者单点 | 协调者宕机后事务悬而未决(in-doubt) | 2PC 天生非容错,协调者保存全局决议 | 参与者一直持锁或占用“prepared”槽位 | 协调者做共识化(Raft/Paxos)复制日志;恢复/重放;人工清理超时 in-doubt |
网络分区/消息丢失 | 有人收到了决议,有人没收到;或一直等不到决议 | 分布式不可靠通信 + 2PC 无法自证全局状态 | 可用性下降(等不到决定就不能推进),甚至运维介入 | 决议落盘后幂等重试广播;参与者定期“拉取决议”;运维回收 |
性能开销 | 额外的两轮网络 RTT + 两次强制落盘 | Prepare 要 fsync(承诺点),Commit 也要落盘 | 延迟上升、吞吐降低,抖动放大 | 批量化/合并事务、网络就近、降低事务跨度;“Presumed Abort/Commit”优化日志 |
运维复杂度 | “悬而未决事务”排查困难 | 跨库跨机房,状态不一致但持锁 | 业务阻塞、SLA 受损 | 统一事务监控;超时/重放工具;灰度回滚策略 |
极端下的启发式决策 | 参与者自作主张回滚/提交 | 长超时 + 业务压力导致“heuristic”操作 | 破坏原子性(最坏) | 严禁启发式提交;只允许回滚启发式并做审计;预案演练 |
为什么 2PC 不完全可靠?
2PC 在安全性(原子性)上依赖“协调者最终能把同一个决议告知所有参与者”;在可用性上会阻塞。一旦协调者长时间不可达、或网络分区持续存在,系统就会陷入悬而未决(in-doubt)状态;如果采用启发式决策(heuristic),则安全性也不再保证。
1. 协调者宕机 → 事务悬挂(in-doubt)
时序窗口:
W1:协调者在收集投票前挂掉 → 没人进入 prepared,安全
W2:所有参与者已返回 YES,协调者尚未把提交决议落盘/广播就挂 → 所有参与者处于 prepared、加锁,不知道后续如何
W3:协调者已写入提交决议且广播给部分参与者后挂 → 有的已提交,有的仍在 prepared(等 待决议)
从最终性看:只要协调者恢复后能补发决议,原子性仍能收敛
从可用性看:未收到决议的一侧长时间阻塞、占锁、顶资源
2. 网络分区/消息丢失
2PC 假设最终可以把同一决议送达所有参与者。当网络长期分区:
一侧已经提交,另一侧还在 prepared;
对外观测会出现“部分提交、部分未提交”的暂态不一致;
只有当网络恢复、做重放/补发后,才会收敛为一致;期间可用性差。
若人为启发式提交/回滚以“解堵”,则破坏原子性(最坏情况)
3. 阻塞特性是协议内生的
prepared 后,参与者必须持锁直到收到决议,写-写并发会被阻塞;
这不是实现细节,而是 2PC 的承诺点设计决定的(保证“接到 Commit 就一定能提交”)
超时问题
超时 ≠ 可以随便回滚。2PC 的正确做法要分处于哪个阶段
1. 定义
超时:一方在合理时限内没有收到对等方应答(参与者等不到协调者的决议,或协调者等不到参与者的投票)
2. 正确处置(分阶段)
阶段 A:投票前(参与者尚未 prepared)
协调者可安全地中止事务(Abort),参与者无副作用
阶段 B:参与者已返回 YES(已写入 prepared)
参与者不得自行提交或回滚(否则破坏原子性)
正确动作:
1. 保持 prepared 与锁(可设置上限时间,避免无限占用)
2. 周期性重试联系协调者
3. 支持“拉取决议”(用全局事务 ID 向协调者/事务记录表查询最终决定)
4. 宕机恢复后,从本地prepared 列表中继续追决议
阶段 C:协调者侧超时
未收到任何参与者的 YES,协调者可以判定失败并发 Rollback
但一旦发现有人已经 YES,必须先把最终决议落盘(commit/abort),再幂等广播
术语 | 含义 | 执行者 | 场景 |
---|---|---|---|
commit | 决议:事务最终执行提交 | 协调者 | 协调者决策阶段(落盘决议) |
abort | 决议:事务最终执行终止 | 协调者 | 协调者决策阶段(落盘决议) |
rollback | 动作:参与者执行 “撤销修改” | 参与者 | 参与者收到 abort 决议后执行 |
3. 错误姿势(会破坏原子性)
参与者在 prepared 状态自行回滚或自行提交(所谓 heuristic 决策)
实践中如必须“解堵”,也只允许启发式回滚(保守选择),且需审计与补偿,但依然要明白这已偏离 2PC 的安全模型。
4. “更先进协议”能否避免阻塞?
3PC(三阶段提交)在 2PC 之上加了一个中间状态(pre-commit)和超时可推进机制,理论上减少阻塞;但在异步网络 + 可能分区模型下,3PC 仍存在安全边界问题,工业界很少单独采用。
Paxos/Raft + 两段提交(又称 Paxos Commit)更常见:
把决议或事务记录写入共识日志,协调者可以无缝切换
参与者可从共识组拉取最终决议,阻塞窗口显著缩小
这并非去掉 2PC,而是把协调者是单点这个薄弱环节用共识加固
协调者故障
1. 问题本质
协调者掌握最终决议的唯一真相;
若它不可达或状态未持久化,参与者就会悬而未决。
2. 改进一:协调者共识化
把协调者的全局决议日志放到 Raft/Paxos:
流程要点:
协调者收齐 YES → 先把提交(或回滚)决议写入共识日志并提交
再向参与者幂等广播该决议
协调者宕机 → 选出新主,凭共识日志里的决议继续补发
参与者重启/超时 → 按事务 ID 拉取决议(而不是干等)
收益:
决议不丢(被多数副本持久化)
协调者不再是单点
in-doubt 时间显著缩短(可用性提升)
3. 改进二:日志恢复 + 决议拉取
协调者恢复:
从共识日志中找出已决定未完播与未决定的事务
对已决定:补发给所有参与者(幂等)
对未决定:根据投票情况与策略统一中止(如 presumed abort)
参与者自救:
启动时扫描本地 prepared 列表
逐个向协调者/事务记录表查询决议并执行
查询不到时按策略退回(只在采用 presumed abort/commit 的系统里,且需与 TM 协议一致)
4. 辅助优化:Presumed Abort / Presumed Commit
Presumed Abort(PA):把默认结局设为回滚(未见到决议就当失败),减少恢复时需要的日志/状态
Presumed Commit(PC):默认提交