2PC、TCC 与 Saga:跨服务状态怎样提交、撤销或补偿
从参与者状态与故障窗口拆解两阶段提交、TCC 和 Saga,解释阻塞、空回滚、悬挂、幂等与不可逆补偿。
关系数据库的本地事务与预备事务提供实现 2PC 的原语,但跨服务语义仍由协调协议定义。Outbox 等替代路径引用 Debezium Outbox。
原子提交的难点是决定已经作出却尚未送达
两阶段提交先让每个参与者执行本地工作并进入 PREPARED,锁住提交所需资源且把恢复信息持久化;协调器收齐 yes 后记录全局 COMMIT,否则记录 ABORT;第二阶段把决定发送给参与者。原子性来自所有参与者服从同一持久决定,不是来自同时执行。
协调器在决定前崩溃可以安全中止或重试 prepare;在 COMMIT 决定落盘后崩溃,参与者不能擅自回滚,只能等待恢复并重放决定。长时间 prepared 会占用锁、连接和日志,协调器存储本身也要复制与恢复。2PC 更适合同一信任域、参与者支持协议且事务短小的场景,不适合把任意 HTTP 服务包装成数据库分支。
TCC 把资源预留变成业务接口
Try 检查并预留资源,Confirm 消耗预留,Cancel 释放预留。余额要区分可用与冻结,库存要有预留记录;只在 Try 里“检查余额够不够”却不冻结,后续 Confirm 仍会竞争失败。Confirm 与 Cancel 都可能重复,必须以全局事务 id + 分支 id 幂等。
Cancel 可能先于迟到的 Try 到达,形成空回滚;Cancel 应记录已取消屏障,后续 Try 发现屏障后不得再预留。Try 超时后又执行会形成悬挂,参与者需要条件写和 fencing 阻止已终止事务复活。TCC 侵入业务模型,换来明确资源边界与可控补偿。
Saga 追加新事实而不是回滚历史
Saga 把长流程拆为多个本地事务,每一步成功后触发下一步;失败时按逆序或业务规则执行补偿。机票已出票、邮件已发送、第三方扣款等动作无法物理撤销,只能产生退票、忽略通知或退款中的新状态。因此 Saga 提供的是可恢复业务一致性,不是隔离性。
编排式 Saga 由协调器保存状态和命令,易于看到全局进度;协同式通过事件推进,耦合较松却难以理解环路和终止。无论形式,都要持久化 step id、attempt、deadline、结果与补偿状态,处理重复、乱序和人工接管。
选择从不变量和锁持有时间开始
必须瞬时保持的金融账本不变量且参与者支持 prepare,可考虑 2PC;可显式预留的短流程可用 TCC;跨组织、长时间、不可逆步骤用 Saga 或异步状态机。Outbox 适合“本地提交后可靠发布”,不直接让多个写入原子。方案评审必须画出每个崩溃点和恢复证据,不能仅比较框架注解。
用证据边界拆开“不知道”和“做不到”
结果未知必须持久化 operationId、目标实体、预期版本和查询入口。自动重试只有在动作尚未开始或目标以相同 id 幂等时安全。用异常类名直接判断“肯定没执行”会在网络断点后制造重复提交。
状态机必须拥有终态、租约和人工出口
每个非终态都要有下一次唤醒来源:消息重投、租约到期、协调器扫描、对账任务或人工队列。只有状态而没有唤醒,流程会永久卡住;只有重试而没有终态,会永久自旋。
协调日志需要回答每个参与者为何停在这里
全局记录至少包含 transactionId、decision、decisionVersion、参与者、分支状态、attempt 与 deadline;参与者记录 prepare/try 结果和最后接受的决定。恢复扫描只能重发相同决定,不能因为当前业务条件变化重新计算 COMMIT 或 ABORT。
测试分别在最后一个 prepare 响应丢失、COMMIT 落盘后未广播、Confirm 成功后响应丢失、Cancel 先于 Try 到达和补偿中途崩溃时杀进程。每次重启后最终状态必须唯一,资源冻结不能无限增长,人工接管能够看到安全动作而不是只看到异常堆栈。
用两个 Java 状态模型固定分布式不变量
下面的 Java 17 模型不模拟真实网络或共识实现,而是把本篇关键版本、租约、状态和容量关系变成确定输出。随后用真实数据库、Broker、锁服务或多进程环境注入延迟、重复、分区与崩溃。
javac --release 17 -Xlint:all -Werror examples/backend-development/distributed-systems/distributed-transactions/TwoPhaseCommitDemo.java examples/backend-development/distributed-systems/distributed-transactions/TccSagaDemo.java
java -cp examples/backend-development/distributed-systems/distributed-transactions TwoPhaseCommitDemo
java -cp examples/backend-development/distributed-systems/distributed-transactions TccSagaDemoparticipants=2 prepared=2 decision=COMMIT coordinatorCrashAfterDecision=true recoveryFromLog=true
tryReserved=10 confirmCalls=2 confirmedOnce=true emptyRollbackIgnored=true compensationState=REFUND_PENDING输出变化时必须解释哪条一致性、epoch、幂等或容量不变量被修改。真实实验还要验证结果未知、旧所有者恢复、状态清理和人工修复路径。
容量、观测与安全要围绕协调成本
同步协调增加往返、日志刷盘、锁持有和 quorum 等待;异步收敛增加积压、版本、去重和对账存储。容量模型至少包含峰值操作率、参与者数、每次尝试、超时窗口、重试放大、状态保留、复制倍数与恢复净速率。平均值无法覆盖分区热点和长尾暂停。
反向实验要打在决定落盘的前后
故障注入必须有范围、自动停止和数据清理。生产演练从只读、单租户、单分片和低比例开始,保留旁路与回滚。完成标准不是组件重新可达,而是业务不变量保持、未知结果已对账、积压按预测下降且旧所有者无法继续写。
演进从缩小协调域开始
小规模优先单库本地事务、模块化单体和单一调度 owner。增长后先按业务冲突域分片、用批量与读模型减少跨域协调,再引入 Outbox、租约或 Saga。不要为了“分布式化”把一个本地不变量拆成跨服务事务。
任何新协调机制都写决策记录:解决什么不变量、网络分区时拒绝什么、权威证据在哪、超时后如何查询、状态保留多久、容量上限、故障演练与移除路径。系统复杂度只有在可验证收益覆盖恢复成本时才值得增加。
数据模型要为并发和恢复保留足够字段
替代方案要比较协调域而不是比较组件列表
替代方案表必须写出失败时的差异:本地事务崩溃自动回滚,2PC 可能停在 prepared,TCC 可能冻结资源,Saga 产生补偿中间态,最终一致产生陈旧窗口,分布式锁产生过期持有者,quorum 产生少数侧拒绝。业务对中间态的容忍度决定方案,而不是框架流行度。
团队门禁要阻止不可恢复的捷径
代码审查要求状态转换与 SQL 条件同时出现,集成测试要求在决定落盘前后杀进程,发布检查要求新旧版本互读,运行看板要求展示最老非终态而不只展示总数。分布式正确性不是某次设计评审的结论,而是持续被反例验证的工程属性。
