分布式事务反直觉坑:两阶段提交也不是银弹
一、强一致不是免费能力
分布式事务常被拿来解决跨服务一致性问题,但两阶段提交并不是银弹。它可以在一定条件下保证多个参与者的原子提交,但会带来阻塞、协调者单点、资源锁定时间长和故障恢复复杂等问题。很多业务场景并不需要强一致事务,硬上 2PC 反而让系统更脆。
两阶段提交分为 prepare 和 commit。协调者先询问所有参与者是否可以提交,参与者预留资源并返回确认;若全部确认,协调者再发送 commit。问题在于,一旦参与者 prepare 后等待 commit,它通常要持有锁和事务状态。如果协调者故障,参与者可能长时间阻塞。
二、2PC 路径:prepare 阶段已经开始持有资源
sequenceDiagram participant C as Coordinator participant A as ServiceA participant B as ServiceB C->>A: prepare C->>B: prepare A-->>C: yes B-->>C: yes C->>A: commit C->>B: commit反直觉的地方在于,强一致并不总是业务最优。订单创建、库存扣减、积分发放、通知发送这些动作的业务要求不同。库存可能需要强约束,通知可以最终一致,积分可以补偿。把所有动作放进一个全局事务,会把最低性能和最高故障复杂度传递给整个链路。
三、补偿任务实现:最终一致必须依赖幂等状态
下面是一个补偿任务结构示例,用于最终一致场景。重点是幂等和状态记录。
def run_compensation(task, handler): if task["status"] == "done": return "skip" try: handler(task["payload"]) task["status"] = "done" except Exception as exc: task["retry_count"] += 1 task["last_error"] = str(exc) if task["retry_count"] > 5: task["status"] = "manual_review" return task["status"]四、方案取舍:Saga、TCC 和 Outbox 都有边界
Saga、TCC、事务消息、Outbox 模式都可以解决部分一致性问题,但都有边界。Saga 依赖补偿动作,补偿不一定能完全撤销;TCC 对业务侵入大;事务消息依赖消息系统可靠性;Outbox 增加本地表和投递链路。选择方案前必须明确一致性要求和失败处理方式。
分布式事务设计的底线,是每个状态都能恢复。不要只设计成功路径。参与者超时、重复请求、部分成功、补偿失败、消息重复都必须处理。真正成熟的系统,不是从不失败,而是失败后能回到可解释状态。
还要把一致性等级写进接口契约。调用方需要知道返回成功意味着什么,是全局提交完成,还是本地事务完成并等待异步补偿。语义不清的接口,会把事务复杂度转嫁给下游。
落地时还要区分技术一致性和业务一致性。技术上全局事务提交成功,并不代表业务状态一定合理;例如优惠券已核销但订单随后被风控拦截,仍需要业务补偿。事务方案只能保证一组写入的提交语义,不能替代业务状态机设计。把状态机画清楚,往往比先选 2PC 还是 Saga 更重要。
生产落地补充:从能跑到可维护
从生产落地角度看,这类方案不能只停留在主流程。更关键的是把输入校验、失败分支、资源上限和回滚路径提前写清楚。主流程通常容易在演示环境里跑通,真正暴露问题的是异常输入、依赖抖动、并发放大和权限边界。一篇技术方案如果没有解释这些约束,读者很难判断它能否放进真实系统。
评估时建议先定义三类指标:正确性指标、稳定性指标和成本指标。正确性指标回答结果是否可信,稳定性指标回答失败时是否可控,成本指标回答持续运行是否划算。三类指标要同时进入验收清单,不能只用平均耗时或单次成功率证明方案有效。
实现层面还需要把观测数据留出来。日志至少包含请求标识、关键参数摘要、耗时、状态和错误类型;指标至少覆盖成功率、超时率、重试次数和队列长度;必要时再补 Trace 关联上下游调用。这样排查问题时不用靠猜,也能区分是代码逻辑、外部依赖还是容量配置导致的故障。
五、总结
两阶段提交能解决部分强一致问题,但不是分布式事务银弹。业务应根据一致性等级选择 2PC、Saga、TCC、事务消息或 Outbox,并把幂等、补偿和故障恢复作为核心设计。