JavaTCCSagaSeataRocketMQ

全局分布式事务协调器实战

B2B强一致性场景:TCC+Saga混合模式实现跨服务、跨公司的分布式事务,库存强一致性的完整解决方案

一、项目概述

1.1 业务背景

在机票/酒店的 B2B 预订场景中,一次订单往往需要同时锁定多个供应商的库存——例如,用户下单一张从北京飞往上海的机票,系统需要同时向国航、东航、南航三家航司的接口发起锁位请求,任何一家失败都需要整体回滚。这类业务的本质是:跨多个外部系统的强一致性操作,任何环节的局部成功都会造成全局数据不一致

项目初期,系统采用传统的分布式锁方案(Redis 分布式锁 + 数据库事务),在单供应商场景下运行稳定。但随着业务扩展到多供应商模式,问题集中爆发:一次订单涉及 3-5 个外部供应商接口,单个供应商的超时(甚至断电不可达)会导致整个订单长时间阻塞,2PC(两阶段提交)协议在这种跨公司、跨网络的场景下更是将所有资源卡死——参与者持有的锁无法释放,导致整条链路的库存被冻结。

1.2 技术选型:TCC vs Saga 模式取舍

TCC 模式

Try-Confirm-Cancel

预留资源阶段(Try)→ 确认使用(Confirm)→ 回滚释放(Cancel)。适用于内部服务、有资源预留能力的场景。优势:强一致、2阶段清晰。劣势:业务侵入性强,每个服务需改造为三阶段接口。

Saga 模式

正向命令链 + 补偿

每个步骤执行正向操作,失败时执行补偿操作(撤销之前的操作)。适用于外部系统、无法改造接口的场景。优势:无锁、资源不冻结。劣势:补偿链复杂、最终一致性。

经过深入评估,最终选择混合策略:内部服务(库存服务、账户服务、订单服务)使用 TCC 模式,通过 Seata AT 模式管理;外部供应商接口使用 Saga 补偿模式,通过 RocketMQ 事务消息协调。这种混合方案的核心理念是:能控制的用 TCC(强一致),不能控制的用 Saga(最终一致)

1.3 业务规模与技术指标

1000+每秒处理订单数
3-5个单订单跨供应商数
<50ms分布式事务平均耗时
99.9%订单成功率(改造后)

系统设计目标:支撑每秒 1000+ 订单的并发处理,单订单平均涉及 3-5 个供应商接口,强一致性保证 99.9% 以上,分布式事务平均耗时控制在 50ms 以内,故障恢复时间(MTTR)不超过 30 秒

💡 架构决策:为什么不用 Seata TCC 统一处理所有场景?因为外部供应商接口不受我们控制——它们不提供 Confirm/Cancel 接口,也不允许我们直接操作它们的数据库。这种情况下,TCC 的"资源预留"机制无法落地,只能退而求其次使用 Saga 的补偿模式。Saga 虽然是最终一致性,但在 B2B 场景下,配合人工介入机制,是可以接受的。

二、技术架构设计

2.1 TCC 模式详解

TCC(Try-Confirm-Cancel)是一种业务层面的两阶段提交协议,将分布式事务的协调能力下沉到应用层,每个参与者需要实现三个接口:

Try 阶段:资源预留

所有参与者同时执行 Try 操作——将资源"冻结"(不扣减库存,只做预留标记),返回预留结果。任何一方 Try 失败,全部回滚各自的预留。

Confirm 阶段:确认使用

所有参与者的 Try 都成功后,TC(Transaction Coordinator)通知所有参与者执行 Confirm——将冻结的库存正式扣减,资源从"预留"状态变为"已使用"。

Cancel 阶段:回滚释放

任意参与者在 Try 或 Confirm 阶段超时/失败,TC 通知所有参与者执行 Cancel——释放预留的库存,资源回归可用状态。

TCC 的核心优势是:资源在 Confirm 之前不会被真正扣减,因此 Confirm 失败的概率极低(理论上只有网络故障),即使 Confirm 失败也可以重试。与 2PC 相比,TCC 的参与者是主动的(实现 Try/Confirm/Cancel),而非被动的(只提交/回滚)。

2.2 Saga 模式详解

Saga 模式将长事务拆分为一系列短事务,每个短事务都有对应的补偿操作。当某一步骤失败时,从后往前依次执行补偿操作(Compensable Transaction)。Saga 的关键假设是:每个正向操作都有对应的补偿操作,且补偿操作是幂等的

与 TCC 的本质区别:Saga 不"预留"资源,而是直接执行正向操作,失败时通过补偿"撤销"。这意味着:正向操作和补偿操作之间存在一个时间窗口,在该窗口内数据处于"中间状态"(机票已锁定但尚未出票)。

2.3 混合架构分层

接入层(API Gateway)

Spring Cloud Gateway → 订单服务 → 事务编排层(Transaction Coordinator)

TCC 事务参与者(内部服务)

库存服务(Try-Confirm-Cancel)| 账户服务 | 积分服务 | 订单状态服务 | Seata TM

Seata TC + MQ 协调层

Seata Server(AT/TCC模式)| RocketMQ 事务消息(外部供应商Saga协调)| 注册中心(Nacos)

Saga 补偿层(外部系统)

航司供应商接口(东航/国航/南航)| 酒店供应商接口 | 外部支付网关 | 最大努力通知队列

基础设施层

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 模式选择决策矩阵

维度TCCSagaSeata 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。

💡 深挖:Try 成功但 Confirm 超时的极限情况:这种情况在分布式环境中极为常见。假设库存服务 Try 成功(库存已冻结),但 Confirm 请求在网络传输中丢失。此时 TC 认为 Confirm 失败,发起重试——重试时库存服务需要判断:是应该 Confirm(因为 Try 成功了),还是已经 Confirm 过(幂等)?答案是通过状态表判断。如果状态是 CONFIRMING,说明之前 Confirm 没完成,此时可以安全重试 Confirm;如果状态是 COMPLETED,说明已经 Confirm 过了,直接返回成功。

挑战二:Saga 补偿链过长的雪崩风险

一次 B2B 机票预订涉及 5 个步骤:①锁定航司A库存 → ②锁定航司B库存(备选) → ③锁定酒店库存 → ④调用支付 → ⑤通知出票。如果步骤 ③ 的补偿执行失败(酒店供应商接口挂了),整个补偿链就卡住了。更危险的是:如果步骤 ④ 的补偿(退款)也失败了,前面所有步骤的资源都被锁定,形成雪崩。

✅ 解决方案:异步补偿队列 + 补偿优先级 + 人工兜底

异步补偿队列:补偿操作不在线程池同步执行(避免占用业务线程),而是通过 Kafka 发送补偿事件到独立队列。补偿消费者以低优先级运行,每个补偿任务有独立的超时(30s)和重试次数(3次)。这样即使某个补偿卡住,也不会阻塞其他事务。

补偿优先级队列:设计补偿顺序时遵循"资源越稀缺,越优先补偿"的原则。机票库存最稀缺(座位不可复制),所以补偿顺序:④支付退款 → ③酒店释放 → ②航司B释放 → ①航司A释放。通过 RocketMQ 的不同 Topic 和 Priority Queue 实现。

补偿失败兜底:补偿重试 3 次仍然失败时,将记录写入"人工补偿待办表"(manual_compensation),触发人工介入流程。同时发送告警通知运营人员。

💡 深挖:机票已锁定但供应商接口已关闭的极限情况:这是 Saga 模式最大的风险——正向操作成功了,但补偿接口不可用。解决方案是"资源超时自动释放":供应商锁位设置 TTL(如 15 分钟),超时后供应商系统自动释放锁位。同时,我们的系统记录"乐观锁定"状态,当检测到供应商接口不可用时,进入"等待自动释放"状态,每分钟检查一次 TTL,15 分钟后自动标记订单失败并通知用户。这种设计将人工介入率从 5% 降至 0.3%。

挑战三:外部供应商接口的不可控超时

外部供应商接口(航司/酒店)完全不受我们控制,它们的超时时间、限流策略、甚至可用性都是未知数。初期方案是直接 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 次。

💡 深挖:"最大努力"的"最大努力"具体指什么?最大努力通知并非无限重试,而是一个有明确定义的工程约束:在保证业务可接受的最大延迟内(通常 24 小时),以指数退避的频率,尽最大努力通知下游系统。超过最大延迟后,系统放弃自动通知,转入人工处理。在 B2B 场景下,最大努力通知的 SLA 是:95% 的通知在 5 分钟内到达,99% 在 1 小时内到达。这个 SLA 通过监控通知到达率(ACK 回执)来衡量。

挑战四: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 模式(减少锁持有时间),同时发送告警。

💡 深挖:Seata AT 模式的性能瓶颈——全局锁 vs 乐观锁?这是分布式事务领域的经典争论。全局锁(Seata AT)的优势是"简单一致"——任何时刻只有一个事务能修改同一行数据,不需要应用层处理冲突。代价是锁等待时间在高并发下不可控。乐观锁(Seata Saga + 应用层重试)的优势是没有锁等待,代价是需要业务层处理冲突(重试逻辑复杂)。经验公式:当单行 QPS < 100 时,用 Seata AT;当 QPS > 100 时,按业务键分库分表 + Seata AT;当 QPS > 500 且冲突率 > 30% 时,考虑降级为 Saga + 乐观锁。

四、关键技术实现

4.1 TCC Try-Confirm-Cancel 完整实现(电商库存场景)

以下代码展示了一个完整的 TCC 参与者实现,用于管理库存的冻结与扣减。每个方法都必须保证幂等性:

TCC 库存服务 - InventoryTccService