# 分布式事务实战:Outbox、Saga、TCC、幂等与补偿
微服务把一次业务操作拆到了多个数据库:订单创建成功、库存扣减成功、支付却失败,系统就会进入不一致状态。分布式事务的目标不是让所有服务“像单库事务一样简单”,而是根据业务不变量选择正确的提交、重试和补偿方式。
## 一、先判断是否真的需要分布式事务
在引入复杂框架前,优先考虑:
1. 把必须原子完成的数据放回同一个服务和数据库;
2. 通过业务流程重新划分聚合边界;
3. 把非关键动作改为异步,如通知、积分和报表;
4. 接受短暂不一致,但规定最大收敛时间。
只有跨服务不变量无法通过边界调整解决时,再选择分布式事务方案。
## 二、两阶段提交为什么不是默认答案
两阶段提交(2PC)由协调者组织 Prepare 和 Commit。它能提供较强的一致性,但参与者需要长时间持有资源;协调者或网络故障时,事务可能阻塞。跨地域、高并发业务通常难以接受它的延迟和可用性代价。
2PC 更适合参与者和基础设施都明确支持、规模受控且强一致优先的场景,而不是所有微服务的通用方案。
## 三、Transactional Outbox:最常用的可靠事件方案
典型流程:
1. 订单服务在同一个本地事务中写入订单表和 outbox_event 表;
2. 独立发布器轮询 Outbox,或通过 CDC 读取数据库变更;
3. 发布器把事件写入消息队列;
4. 消费者处理事件并记录幂等结果;
5. 发布成功的 Outbox 记录被标记或归档;
6. 定时任务扫描超时事件并告警、重试。
关键点是“业务数据与待发送事件同事务提交”。消息中间件即使短暂不可用,事件仍在数据库中,不会出现业务成功但消息永久丢失。Outbox 通常保证至少一次投递,因此消费者必须幂等,不能假设消息只到一次。
Outbox 表至少应包含事件 ID、聚合 ID、事件类型、载荷、状态、重试次数、下次重试时间、创建和发布时间。对大表要按时间归档并监控未发布事件数量。
## 四、Saga:长事务的正向步骤与补偿步骤
Saga 把长事务拆成多个本地事务:
- 创建订单 → 补偿:取消订单;
- 预占库存 → 补偿:释放库存;
- 冻结余额 → 补偿:解冻余额;
- 创建发货单 → 补偿:关闭发货单。
两种组织方式:
- 编排式:由一个流程编排器决定下一步,状态清晰,适合复杂流程;
- 事件协同式:服务通过事件推进流程,耦合低,但链路难以理解和排查。
补偿不等于数据库回滚。邮件无法“撤回”,已发货商品也不能简单删除记录。补偿必须符合业务语义,例如退款、冲正、创建反向流水,并保留审计记录。
## 五、TCC:业务级资源预留
TCC 包含:
- Try:检查并预留资源;
- Confirm:确认使用预留资源;
- Cancel:释放预留资源。
例如库存 Try 把可用数量转为冻结数量,Confirm 扣减冻结数量,Cancel 归还冻结数量。
生产实现必须处理三类异常:
1. 空回滚:Try 未成功或请求未到达,Cancel 却先到;
2. 幂等:Confirm 或 Cancel 可能被重复调用;
3. 悬挂:Cancel 已执行后,迟到的 Try 又成功。
因此每个分支事务都要保存全局事务号、分支号和状态,并使用状态机拒绝非法迁移。TCC 侵入业务较深,适合资金、库存等对资源预留有明确模型的核心链路。
## 六、幂等才是重试的前提
建议为每个写请求携带业务幂等键,例如 payment_request_id。服务端可以组合使用:
- 数据库唯一约束;
- 幂等请求表;
- 业务状态机;
- 乐观锁版本号;
- 已处理消息表。
消费端可在同一事务中先插入 processed_message(message_id),唯一键冲突就说明已经处理,直接返回成功;插入成功后再执行业务更新并提交。不要使用“先查询、后插入”作为唯一防线,并发请求可能同时查询不到。最终仍需唯一约束或原子条件更新兜底。
## 七、重试、死信与对账
可靠链路应设置:
- 指数退避和随机抖动;
- 最大重试次数和总时间;
- 可重试错误与永久错误分类;
- 死信队列和人工处理入口;
- 业务对账任务;
- 可按事件 ID 回放;
- 全链路追踪和状态查询。
对账不是失败后的临时脚本,而是最终一致性系统的一部分。对账应比较业务事实,例如支付平台成功流水与本地订单状态,而不是只比较消息是否发送。
## 八、方案选择建议
- 同库内操作:优先本地事务;
- 业务成功后可靠发事件:Outbox + CDC/轮询;
- 多步骤、执行时间长、允许补偿:Saga;
- 核心资源必须预留:TCC;
- 基础设施受控且必须强一致:谨慎评估 2PC。
## 九、上线检查清单
1. 每一步的业务不变量是什么?
2. 请求和消息是否都有全局唯一 ID?
3. 所有重试操作是否幂等?
4. 补偿失败是否还能继续重试?
5. 状态机是否拒绝逆序和非法操作?
6. 消息乱序、重复、延迟和丢失是否演练?
7. 能否从业务 ID 查到完整执行轨迹?
8. 是否有对账、回放和人工修复工具?
分布式事务真正的难点不在“调用哪个框架”,而在于把状态、失败和恢复设计成业务的一部分。做到可重试、可补偿、可对账和可审计,系统才具备生产可用性。