分布式事务 2PC 与 3PC从协调者单点到网络分区容错的协议演进一、分布式事务的一致性困境为什么本地提交不够用分布式系统中一个事务可能涉及多个节点的数据修改。最简单的方案是每个节点独立提交——但节点 A 提交成功、节点 B 提交失败时数据不一致。两阶段提交2PC通过引入协调者Coordinator统一控制提交决策保证所有节点要么全部提交、要么全部回滚。但 2PC 有一个致命缺陷协调者在第二阶段崩溃时参与者Participant会无限阻塞等待——既不能提交可能其他参与者收到回滚指令也不能回滚可能其他参与者收到提交指令。三阶段提交3PC通过增加预提交阶段和超时机制试图解决阻塞问题但引入了新的复杂性。二、2PC 与 3PC 协议机制对比sequenceDiagram participant C as 协调者 participant P1 as 参与者1 participant P2 as 参与者2 Note over C,P2: 2PC 协议 C-P1: Phase 1: PREPARE C-P2: Phase 1: PREPARE P1--C: VOTE_COMMIT P2--C: VOTE_COMMIT C-P1: Phase 2: GLOBAL_COMMIT C-P2: Phase 2: GLOBAL_COMMIT P1--C: ACK P2--C: ACK Note over C,P2: 3PC 协议 C-P1: Phase 1: CAN_COMMIT C-P2: Phase 1: CAN_COMMIT P1--C: YES P2--C: YES C-P1: Phase 2: PRE_COMMIT C-P2: Phase 2: PRE_COMMIT P1--C: ACK P2--C: ACK C-P1: Phase 3: DO_COMMIT C-P2: Phase 3: DO_COMMIT P1--C: DONE P2--C: DONE2PC 的两个阶段Prepare投票和 Commit/Abort执行。3PC 的三个阶段CanCommit询问、PreCommit预提交和 DoCommit提交。3PC 的 PreCommit 阶段让参与者在超时后可以安全提交——因为所有参与者已经确认可以提交。三、工程实现2PC 协调者与故障恢复3.1 2PC 协调者实现public class TwoPhaseCoordinator { private final ListParticipantClient participants; private final TransactionLogStore logStore; public CoordinationResult coordinate(Transaction txn) { String txnId txn.getId(); // Phase 1: PREPARE - 询问所有参与者是否可以提交 logStore.log(txnId, PREPARE_START); int yesVotes 0; ListParticipantClient prepared new ArrayList(); for (ParticipantClient p : participants) { try { Vote vote p.prepare(txnId, txn.getOperations(p.getId())); if (vote Vote.COMMIT) { yesVotes; prepared.add(p); } else { // 任一参与者投 ABORT全局回滚 logStore.log(txnId, VOTE_ABORT: p.getId()); return abortAll(txnId, participants); } } catch (Exception e) { // 参与者无响应视为 ABORT logStore.log(txnId, VOTE_TIMEOUT: p.getId()); return abortAll(txnId, participants); } } // Phase 2: COMMIT - 所有参与者投了 YES if (yesVotes participants.size()) { logStore.log(txnId, GLOBAL_COMMIT); return commitAll(txnId, participants); } return abortAll(txnId, participants); } private CoordinationResult commitAll(String txnId, ListParticipantClient participants) { int committed 0; for (ParticipantClient p : participants) { try { p.commit(txnId); committed; } catch (Exception e) { // 提交失败需要重试直到成功 // 2PC 的承诺一旦决定提交必须最终提交 logStore.log(txnId, COMMIT_RETRY: p.getId()); scheduleRetry(txnId, p, commit); } } return new CoordinationResult( committed participants.size() ? Status.COMMITTED : Status.COMMITTING, txnId ); } private CoordinationResult abortAll(String txnId, ListParticipantClient participants) { logStore.log(txnId, GLOBAL_ABORT); for (ParticipantClient p : participants) { try { p.abort(txnId); } catch (Exception e) { scheduleRetry(txnId, p, abort); } } return new CoordinationResult(Status.ABORTED, txnId); } }3.2 参与者实现与故障恢复public class Participant { private final TransactionLogStore logStore; private final DataManager dataManager; // 处理 PREPARE 请求 public Vote prepare(String txnId, ListOperation ops) { // 检查是否已经处理过该事务 TransactionState state logStore.getState(txnId); if (state TransactionState.PREPARED) { return Vote.COMMIT; // 幂等已准备返回 YES } if (state TransactionState.COMMITTED) { return Vote.COMMIT; // 已提交返回 YES } // 执行操作但不提交写入 Undo Log try { dataManager.executeAndLog(txnId, ops); logStore.log(txnId, PREPARED); return Vote.COMMIT; } catch (Exception e) { logStore.log(txnId, PREPARE_FAILED); return Vote.ABORT; } } // 处理 COMMIT 请求 public void commit(String txnId) { TransactionState state logStore.getState(txnId); if (state TransactionState.COMMITTED) { return; // 幂等已提交 } // 提交本地事务清除 Undo Log dataManager.commit(txnId); logStore.log(txnId, COMMITTED); } // 处理 ABORT 请求 public void abort(String txnId) { TransactionState state logStore.getState(txnId); if (state TransactionState.ABORTED) { return; // 幂等已回滚 } // 回滚本地事务使用 Undo Log 恢复 dataManager.rollback(txnId); logStore.log(txnId, ABORTED); } }3.3 协调者故障恢复public class CoordinatorRecovery { private final TransactionLogStore logStore; private final ListParticipantClient participants; public void recover() { // 扫描所有未完成的事务日志 ListTransactionLog incomplete logStore.findIncomplete(); for (TransactionLog log : incomplete) { switch (log.getLastState()) { case PREPARE_START: // Phase 1 中崩溃可能已有参与者 PREPARED // 需要询问所有参与者的状态 recoverFromPrepare(log.getTxnId()); break; case GLOBAL_COMMIT: // Phase 2 中崩溃重试提交 commitAll(log.getTxnId(), participants); break; case GLOBAL_ABORT: // Phase 2 中崩溃重试回滚 abortAll(log.getTxnId(), participants); break; } } } private void recoverFromPrepare(String txnId) { // 询问所有参与者的投票结果 int yesVotes 0; for (ParticipantClient p : participants) { try { ParticipantState state p.getState(txnId); if (state ParticipantState.PREPARED) { yesVotes; } } catch (Exception e) { // 参与者不可达无法确定投票结果 // 保守策略全局回滚 abortAll(txnId, participants); return; } } if (yesVotes participants.size()) { commitAll(txnId, participants); } else { abortAll(txnId, participants); } } }四、2PC/3PC 的根本局限与工程权衡2PC 的阻塞问题协调者在 Phase 2 崩溃时已 PREPARED 的参与者持有锁和资源无法释放。如果协调者长时间无法恢复这些资源被锁定可能影响其他事务。这是 2PC 在高可用场景下不被推荐的根本原因。3PC 的网络分区问题3PC 通过超时机制解决阻塞但在网络分区场景下可能导致数据不一致——分区一端的参与者在超时后提交分区另一端的参与者在超时后回滚。3PC 在理论上解决了阻塞问题但引入了更严重的一致性问题。性能开销的叠加效应2PC 需要两轮 RPCPrepare Commit3PC 需要三轮。每轮 RPC 的延迟在跨数据中心场景下可能达到数十毫秒三轮 RPC 的总延迟可能超过 100ms。对于低延迟要求的业务2PC/3PC 的延迟开销不可接受。协调者的单点瓶颈协调者是有状态的单点——所有事务决策依赖协调者。协调者故障时所有进行中的事务阻塞。生产环境通常使用协调者集群如 Seata TC 集群和 Raft 选举来保证可用性但集群本身增加了系统复杂度。五、总结2PC 和 3PC 是分布式事务的理论基础核心目标是在多个节点间保证原子性——要么全部提交要么全部回滚。2PC 简单但存在阻塞风险3PC 通过超时解决阻塞但引入网络分区下的一致性问题。生产环境中纯粹的 2PC/3PC 实现较少使用更多采用基于 2PC 的变体如 Seata AT、XA 协议或最终一致性方案如 TCC、Saga。落地时需重点关注三个参数Prepare 超时时间建议 30 秒、Commit 重试次数建议无限重试直到成功、协调者故障恢复时间建议 30 秒。建议在强一致性要求高的场景如金融交易使用 2PC 变体在可用性优先的场景使用最终一致性方案。
分布式事务 2PC 与 3PC:从协调者单点到网络分区容错的协议演进
发布时间:2026/6/14 17:11:32
分布式事务 2PC 与 3PC从协调者单点到网络分区容错的协议演进一、分布式事务的一致性困境为什么本地提交不够用分布式系统中一个事务可能涉及多个节点的数据修改。最简单的方案是每个节点独立提交——但节点 A 提交成功、节点 B 提交失败时数据不一致。两阶段提交2PC通过引入协调者Coordinator统一控制提交决策保证所有节点要么全部提交、要么全部回滚。但 2PC 有一个致命缺陷协调者在第二阶段崩溃时参与者Participant会无限阻塞等待——既不能提交可能其他参与者收到回滚指令也不能回滚可能其他参与者收到提交指令。三阶段提交3PC通过增加预提交阶段和超时机制试图解决阻塞问题但引入了新的复杂性。二、2PC 与 3PC 协议机制对比sequenceDiagram participant C as 协调者 participant P1 as 参与者1 participant P2 as 参与者2 Note over C,P2: 2PC 协议 C-P1: Phase 1: PREPARE C-P2: Phase 1: PREPARE P1--C: VOTE_COMMIT P2--C: VOTE_COMMIT C-P1: Phase 2: GLOBAL_COMMIT C-P2: Phase 2: GLOBAL_COMMIT P1--C: ACK P2--C: ACK Note over C,P2: 3PC 协议 C-P1: Phase 1: CAN_COMMIT C-P2: Phase 1: CAN_COMMIT P1--C: YES P2--C: YES C-P1: Phase 2: PRE_COMMIT C-P2: Phase 2: PRE_COMMIT P1--C: ACK P2--C: ACK C-P1: Phase 3: DO_COMMIT C-P2: Phase 3: DO_COMMIT P1--C: DONE P2--C: DONE2PC 的两个阶段Prepare投票和 Commit/Abort执行。3PC 的三个阶段CanCommit询问、PreCommit预提交和 DoCommit提交。3PC 的 PreCommit 阶段让参与者在超时后可以安全提交——因为所有参与者已经确认可以提交。三、工程实现2PC 协调者与故障恢复3.1 2PC 协调者实现public class TwoPhaseCoordinator { private final ListParticipantClient participants; private final TransactionLogStore logStore; public CoordinationResult coordinate(Transaction txn) { String txnId txn.getId(); // Phase 1: PREPARE - 询问所有参与者是否可以提交 logStore.log(txnId, PREPARE_START); int yesVotes 0; ListParticipantClient prepared new ArrayList(); for (ParticipantClient p : participants) { try { Vote vote p.prepare(txnId, txn.getOperations(p.getId())); if (vote Vote.COMMIT) { yesVotes; prepared.add(p); } else { // 任一参与者投 ABORT全局回滚 logStore.log(txnId, VOTE_ABORT: p.getId()); return abortAll(txnId, participants); } } catch (Exception e) { // 参与者无响应视为 ABORT logStore.log(txnId, VOTE_TIMEOUT: p.getId()); return abortAll(txnId, participants); } } // Phase 2: COMMIT - 所有参与者投了 YES if (yesVotes participants.size()) { logStore.log(txnId, GLOBAL_COMMIT); return commitAll(txnId, participants); } return abortAll(txnId, participants); } private CoordinationResult commitAll(String txnId, ListParticipantClient participants) { int committed 0; for (ParticipantClient p : participants) { try { p.commit(txnId); committed; } catch (Exception e) { // 提交失败需要重试直到成功 // 2PC 的承诺一旦决定提交必须最终提交 logStore.log(txnId, COMMIT_RETRY: p.getId()); scheduleRetry(txnId, p, commit); } } return new CoordinationResult( committed participants.size() ? Status.COMMITTED : Status.COMMITTING, txnId ); } private CoordinationResult abortAll(String txnId, ListParticipantClient participants) { logStore.log(txnId, GLOBAL_ABORT); for (ParticipantClient p : participants) { try { p.abort(txnId); } catch (Exception e) { scheduleRetry(txnId, p, abort); } } return new CoordinationResult(Status.ABORTED, txnId); } }3.2 参与者实现与故障恢复public class Participant { private final TransactionLogStore logStore; private final DataManager dataManager; // 处理 PREPARE 请求 public Vote prepare(String txnId, ListOperation ops) { // 检查是否已经处理过该事务 TransactionState state logStore.getState(txnId); if (state TransactionState.PREPARED) { return Vote.COMMIT; // 幂等已准备返回 YES } if (state TransactionState.COMMITTED) { return Vote.COMMIT; // 已提交返回 YES } // 执行操作但不提交写入 Undo Log try { dataManager.executeAndLog(txnId, ops); logStore.log(txnId, PREPARED); return Vote.COMMIT; } catch (Exception e) { logStore.log(txnId, PREPARE_FAILED); return Vote.ABORT; } } // 处理 COMMIT 请求 public void commit(String txnId) { TransactionState state logStore.getState(txnId); if (state TransactionState.COMMITTED) { return; // 幂等已提交 } // 提交本地事务清除 Undo Log dataManager.commit(txnId); logStore.log(txnId, COMMITTED); } // 处理 ABORT 请求 public void abort(String txnId) { TransactionState state logStore.getState(txnId); if (state TransactionState.ABORTED) { return; // 幂等已回滚 } // 回滚本地事务使用 Undo Log 恢复 dataManager.rollback(txnId); logStore.log(txnId, ABORTED); } }3.3 协调者故障恢复public class CoordinatorRecovery { private final TransactionLogStore logStore; private final ListParticipantClient participants; public void recover() { // 扫描所有未完成的事务日志 ListTransactionLog incomplete logStore.findIncomplete(); for (TransactionLog log : incomplete) { switch (log.getLastState()) { case PREPARE_START: // Phase 1 中崩溃可能已有参与者 PREPARED // 需要询问所有参与者的状态 recoverFromPrepare(log.getTxnId()); break; case GLOBAL_COMMIT: // Phase 2 中崩溃重试提交 commitAll(log.getTxnId(), participants); break; case GLOBAL_ABORT: // Phase 2 中崩溃重试回滚 abortAll(log.getTxnId(), participants); break; } } } private void recoverFromPrepare(String txnId) { // 询问所有参与者的投票结果 int yesVotes 0; for (ParticipantClient p : participants) { try { ParticipantState state p.getState(txnId); if (state ParticipantState.PREPARED) { yesVotes; } } catch (Exception e) { // 参与者不可达无法确定投票结果 // 保守策略全局回滚 abortAll(txnId, participants); return; } } if (yesVotes participants.size()) { commitAll(txnId, participants); } else { abortAll(txnId, participants); } } }四、2PC/3PC 的根本局限与工程权衡2PC 的阻塞问题协调者在 Phase 2 崩溃时已 PREPARED 的参与者持有锁和资源无法释放。如果协调者长时间无法恢复这些资源被锁定可能影响其他事务。这是 2PC 在高可用场景下不被推荐的根本原因。3PC 的网络分区问题3PC 通过超时机制解决阻塞但在网络分区场景下可能导致数据不一致——分区一端的参与者在超时后提交分区另一端的参与者在超时后回滚。3PC 在理论上解决了阻塞问题但引入了更严重的一致性问题。性能开销的叠加效应2PC 需要两轮 RPCPrepare Commit3PC 需要三轮。每轮 RPC 的延迟在跨数据中心场景下可能达到数十毫秒三轮 RPC 的总延迟可能超过 100ms。对于低延迟要求的业务2PC/3PC 的延迟开销不可接受。协调者的单点瓶颈协调者是有状态的单点——所有事务决策依赖协调者。协调者故障时所有进行中的事务阻塞。生产环境通常使用协调者集群如 Seata TC 集群和 Raft 选举来保证可用性但集群本身增加了系统复杂度。五、总结2PC 和 3PC 是分布式事务的理论基础核心目标是在多个节点间保证原子性——要么全部提交要么全部回滚。2PC 简单但存在阻塞风险3PC 通过超时解决阻塞但引入网络分区下的一致性问题。生产环境中纯粹的 2PC/3PC 实现较少使用更多采用基于 2PC 的变体如 Seata AT、XA 协议或最终一致性方案如 TCC、Saga。落地时需重点关注三个参数Prepare 超时时间建议 30 秒、Commit 重试次数建议无限重试直到成功、协调者故障恢复时间建议 30 秒。建议在强一致性要求高的场景如金融交易使用 2PC 变体在可用性优先的场景使用最终一致性方案。