全局分布式事务协调器实战
B2B强一致性场景:TCC+Saga混合模式实现跨服务、跨公司的分布式事务,库存强一致性的完整解决方案
一、项目概述
1.1 业务背景
在机票/酒店的 B2B 预订场景中,一次订单往往需要同时锁定多个供应商的库存——例如,用户下单一张从北京飞往上海的机票,系统需要同时向国航、东航、南航三家航司的接口发起锁位请求,任何一家失败都需要整体回滚。这类业务的本质是:跨多个外部系统的强一致性操作,任何环节的局部成功都会造成全局数据不一致。
项目初期,系统采用传统的分布式锁方案(Redis 分布式锁 + 数据库事务),在单供应商场景下运行稳定。但随着业务扩展到多供应商模式,问题集中爆发:一次订单涉及 3-5 个外部供应商接口,单个供应商的超时(甚至断电不可达)会导致整个订单长时间阻塞,2PC(两阶段提交)协议在这种跨公司、跨网络的场景下更是将所有资源卡死——参与者持有的锁无法释放,导致整条链路的库存被冻结。
1.2 技术选型:TCC vs Saga 模式取舍
Try-Confirm-Cancel
预留资源阶段(Try)→ 确认使用(Confirm)→ 回滚释放(Cancel)。适用于内部服务、有资源预留能力的场景。优势:强一致、2阶段清晰。劣势:业务侵入性强,每个服务需改造为三阶段接口。
正向命令链 + 补偿
每个步骤执行正向操作,失败时执行补偿操作(撤销之前的操作)。适用于外部系统、无法改造接口的场景。优势:无锁、资源不冻结。劣势:补偿链复杂、最终一致性。
经过深入评估,最终选择混合策略:内部服务(库存服务、账户服务、订单服务)使用 TCC 模式,通过 Seata AT 模式管理;外部供应商接口使用 Saga 补偿模式,通过 RocketMQ 事务消息协调。这种混合方案的核心理念是:能控制的用 TCC(强一致),不能控制的用 Saga(最终一致)。
1.3 业务规模与技术指标
系统设计目标:支撑每秒 1000+ 订单的并发处理,单订单平均涉及 3-5 个供应商接口,强一致性保证 99.9% 以上,分布式事务平均耗时控制在 50ms 以内,故障恢复时间(MTTR)不超过 30 秒。
二、技术架构设计
2.1 TCC 模式详解
TCC(Try-Confirm-Cancel)是一种业务层面的两阶段提交协议,将分布式事务的协调能力下沉到应用层,每个参与者需要实现三个接口:
所有参与者同时执行 Try 操作——将资源"冻结"(不扣减库存,只做预留标记),返回预留结果。任何一方 Try 失败,全部回滚各自的预留。
所有参与者的 Try 都成功后,TC(Transaction Coordinator)通知所有参与者执行 Confirm——将冻结的库存正式扣减,资源从"预留"状态变为"已使用"。
任意参与者在 Try 或 Confirm 阶段超时/失败,TC 通知所有参与者执行 Cancel——释放预留的库存,资源回归可用状态。
TCC 的核心优势是:资源在 Confirm 之前不会被真正扣减,因此 Confirm 失败的概率极低(理论上只有网络故障),即使 Confirm 失败也可以重试。与 2PC 相比,TCC 的参与者是主动的(实现 Try/Confirm/Cancel),而非被动的(只提交/回滚)。
2.2 Saga 模式详解
Saga 模式将长事务拆分为一系列短事务,每个短事务都有对应的补偿操作。当某一步骤失败时,从后往前依次执行补偿操作(Compensable Transaction)。Saga 的关键假设是:每个正向操作都有对应的补偿操作,且补偿操作是幂等的。
与 TCC 的本质区别:Saga 不"预留"资源,而是直接执行正向操作,失败时通过补偿"撤销"。这意味着:正向操作和补偿操作之间存在一个时间窗口,在该窗口内数据处于"中间状态"(机票已锁定但尚未出票)。
2.3 混合架构分层
Spring Cloud Gateway → 订单服务 → 事务编排层(Transaction Coordinator)
库存服务(Try-Confirm-Cancel)| 账户服务 | 积分服务 | 订单状态服务 | Seata TM
Seata Server(AT/TCC模式)| RocketMQ 事务消息(外部供应商Saga协调)| 注册中心(Nacos)
航司供应商接口(东航/国航/南航)| 酒店供应商接口 | 外部支付网关 | 最大努力通知队列
MySQL(undo log)| Redis(幂等锁)| Kafka(补偿事件)| Prometheus + Grafana | Sentinel
2.4 Seata AT 模式架构
Seata AT(Automatic Transaction)模式是 Seata 最易用的模式,适用于我们内部的微服务场景。核心原理:
- 全局锁(Global Lock):Seata Server 维护全局锁表,任何分支事务修改数据前必须先申请全局锁,避免分布式并发下的脏写
- Undo Log:每个分支事务执行 SQL 时,自动记录"回滚日志"(修改前的数据快照)到 undo_log 表,提交前由 TC 统一协调
- 分支事务注册:TM(Transaction Manager)向 TC 注册全局事务和分支事务,TC 维护全局事务状态
2.5 模式选择决策矩阵
| 维度 | TCC | Saga | Seata AT | 本地消息表 |
|---|---|---|---|---|
| 一致性类型 | 强一致性 | 最终一致性 | 强一致性 | 最终一致性 |
| 资源锁定 | Try阶段预留,不锁表 | 无锁定 | 全局锁(行级) | 无锁定 |
| 业务侵入性 | 高(需改三接口) | 中(需写补偿逻辑) | 低(自动拦截SQL) | 低 |
| 适用场景 | 内部服务、有预留能力 | 外部系统、无补偿接口 | 内部微服务、MySQL | 异步解耦场景 |
| 性能损耗 | 低 | 最低 | 中(全局锁竞争) | 低 |
| 本次使用 | ✅ 内部库存服务 | ✅ 外部供应商 | ✅ 内部服务组 | —— |
三、核心技术挑战与解决方案
挑战一:TCC 的幂等、空回滚与悬挂问题
TCC 模式有三个经典的"坑":幂等(Confirm/Cancel 可能被重复调用)、空回滚(Try 未执行就收到 Cancel)、悬挂(Cancel 比 Try 先执行)。这三个问题在分布式环境下极容易触发。以"悬挂"为例:如果 Try 成功但 Confirm 超时了,TC 不知道该Confirm还是Cancel,此时资源被锁定但事务状态未知。
✅ 解决方案:状态机驱动 + 分布式幂等锁 + 防悬挂检测
幂等控制:为每个全局事务生成唯一的 xid,存入事务状态表 trans_state(xid, status, create_time)。每个分支的 Try/Confirm/Cancel 操作前,先查状态表:只有当状态为"TRYING"时才执行 Try,状态为"CONFIRMING"时才执行 Confirm。通过数据库唯一索引保证幂等。
空回滚检测:TC 在发送 Cancel 指令前,先查询事务状态表。如果该分支从未执行过 Try(状态不在表中),说明是空回滚,直接记录 STATUS_ROLLBACKED 并返回成功,不实际执行 Cancel。
悬挂检测与恢复:引入定时任务扫描 trans_state 表中超时的"TRYING"状态记录(超过 30 秒未推进)。对这些记录执行"最大努力确认"策略:如果对应资源尚在预留状态,则推进 Confirm;如果资源已释放,则标记 Cancel。Try 超时 60 秒自动触发 Cancel。
挑战二:Saga 补偿链过长的雪崩风险
一次 B2B 机票预订涉及 5 个步骤:①锁定航司A库存 → ②锁定航司B库存(备选) → ③锁定酒店库存 → ④调用支付 → ⑤通知出票。如果步骤 ③ 的补偿执行失败(酒店供应商接口挂了),整个补偿链就卡住了。更危险的是:如果步骤 ④ 的补偿(退款)也失败了,前面所有步骤的资源都被锁定,形成雪崩。
✅ 解决方案:异步补偿队列 + 补偿优先级 + 人工兜底
异步补偿队列:补偿操作不在线程池同步执行(避免占用业务线程),而是通过 Kafka 发送补偿事件到独立队列。补偿消费者以低优先级运行,每个补偿任务有独立的超时(30s)和重试次数(3次)。这样即使某个补偿卡住,也不会阻塞其他事务。
补偿优先级队列:设计补偿顺序时遵循"资源越稀缺,越优先补偿"的原则。机票库存最稀缺(座位不可复制),所以补偿顺序:④支付退款 → ③酒店释放 → ②航司B释放 → ①航司A释放。通过 RocketMQ 的不同 Topic 和 Priority Queue 实现。
补偿失败兜底:补偿重试 3 次仍然失败时,将记录写入"人工补偿待办表"(manual_compensation),触发人工介入流程。同时发送告警通知运营人员。
挑战三:外部供应商接口的不可控超时
外部供应商接口(航司/酒店)完全不受我们控制,它们的超时时间、限流策略、甚至可用性都是未知数。初期方案是直接 HTTP 调用,结果在促销高峰期,30% 的订单因为单个供应商超时而被整体拖累,超时时间从 500ms 到 30s 不等,完全不可预测。
✅ 解决方案:RocketMQ 事务消息 + 最大努力通知
RocketMQ 事务消息核心流程:① 应用执行本地事务(锁定供应商库存)并发送 half 消息(对 consumer 不可见)→ ② MQ Broker 回调应用确认本地事务状态(commit 或 rollback)→ ③ 如果本地事务成功,MQ 将 half 消息投递为 normal 消息 → ④ 外部供应商系统消费消息并执行业务操作 → ⑤ 如果超时未收到响应,MQ Broker 执行回查(check)接口确认最终状态。
最大努力通知(Best-Effort Notification):当 MQ 事务消息仍然无法到达供应商时(供应商系统彻底宕机),触发最大努力通知:通过定时任务定期轮询供应商接口状态(每 5 分钟),直到确认供应商已处理或超过重试上限(24 小时)。通知策略:指数退避(1min → 2min → 4min → ... → 24h),共重试 12 次。
挑战四:Seata 全局锁竞争与性能瓶颈
Seata AT 模式的全局锁机制在高并发场景下暴露了性能问题:当多个订单同时修改同一批库存(如热卖航班座位)时,全局锁成为瓶颈。实测发现,当单个分片的并发写超过 500 QPS 时,Seata Server 的 P99 锁等待时间从 5ms 飙升到 200ms,直接导致整体吞吐量下降 60%。
✅ 解决方案:Seata Server 分片 + 业务键隔离 + 热点检测
Seata Server 集群分片:将 Seata Server 部署为集群模式(3 台 Server + Nacos 注册),按业务键(flight_id + date)做分片路由,不同分片路由到不同的 TC 实例。分片键 = Hash(flight_id + date) % N,确保同一航班的订单路由到同一个 TC,避免跨实例锁冲突。
业务键隔离(消除热点行锁):对于热卖航班座位,采用"乐观锁 + 版本号"替代 Seata 全局锁:UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE flight_id = ? AND version = ?。乐观锁在高冲突场景下性能更好(无锁等待),但低冲突场景下不如 Seata AT(Seata 的全局锁在高并发下比乐观锁重试更稳定)。
热点检测与自动降级:通过 Sentinel 监控每个分片键的锁等待时间和 QPS,当锁等待 P99 > 50ms 时,自动将该分片的 Seata AT 模式降级为 Saga 模式(减少锁持有时间),同时发送告警。
四、关键技术实现
4.1 TCC Try-Confirm-Cancel 完整实现(电商库存场景)
以下代码展示了一个完整的 TCC 参与者实现,用于管理库存的冻结与扣减。每个方法都必须保证幂等性: