摘要:微服务拆分后,一次业务操作往往需要同时修改订单、库存、支付、积分等多个服务的数据。单机数据库事务无法跨越服务边界,分布式事务因此成为系统设计中绕不开的问题。本文从下单扣库存场景出发,系统讲解分布式事务的核心概念、CAP 与 BASE、XA 和 2PC、TCC、Saga、Transactional Outbox、本地消息表、RocketMQ 事务消息、最大努力通知,以及 Apache Seata 的 AT、XA、TCC、Saga 四种模式。文章同时给出方案选型、幂等设计、补偿机制、事务防悬挂和生产排查方法。
写在前面:数据库事务失效的那一刻
单体应用里,创建订单和扣减库存可能只是一个普通方法:
@Transactional
public Long createOrder(CreateOrderCommand command) {
stockRepository.deduct(command.skuId(), command.quantity());
Order order = orderRepository.save(Order.create(command));
return order.getId();
}订单表和库存表都在同一个数据库中,Spring 开启一个本地事务,两条 SQL 要么一起提交,要么一起回滚。只要数据库支持 ACID,这件事没有太多悬念。
业务增长以后,订单和库存通常会拆成独立服务:Order Service和Stock Service。

麻烦从这里开始:
如果订单创建成功,库存扣减失败,系统里会出现一个无法履约的订单。
如果库存扣减成功,但 Order Service 在响应前超时,调用方不知道这笔订单究竟有没有成功。
如果库存服务已经扣减,订单服务随后崩溃,库存应该回滚,还是等待订单服务恢复后继续完成?
这些问题无法再靠一个 @Transactional 解决。因为数据库本地事务只能管理自己的连接,不能自动控制另一个服务、另一个数据库或一个远程支付系统。
分布式事务要解决的,就是跨服务、跨数据库、跨资源的一致性问题。
但先说一个重要结论:分布式事务不等于“想办法让所有系统像单库事务一样同时提交”。在微服务里,很多业务更适合接受短暂不一致,通过状态机、消息、补偿和对账最终收敛。
真正的难点不是背方案名称,而是判断业务究竟需要哪种一致性。
一、什么是分布式事务
事务是一组操作构成的执行单元。对外看,这些操作应该像一个整体:
要么全部成功。
要么全部失败。
或者在无法立即完成时,进入可恢复的中间状态,最终收敛到成功或失败。
本地事务通常发生在单个数据库内部:
BEGIN;
UPDATE stock
SET available = available - 1
WHERE sku_id = 1001
AND available >= 1;
INSERT INTO orders(order_id, sku_id, status)
VALUES (90001, 1001, 'CREATED');
COMMIT;分布式事务则跨越多个独立资源:

它们可能使用不同数据库,运行在不同机器上,甚至由不同公司维护。网络随时可能超时、重试、断开。某个请求没有收到响应,不代表对方没有执行;对方返回成功,也不代表后续动作一定完成。
因此,分布式事务比本地事务多了几个现实约束:
网络不是可靠内存调用。
超时不等于失败。
重试可能造成重复执行。
服务可能在任何阶段重启。
消息可能重复、延迟或乱序。
补偿操作本身也可能失败。
外部系统未必提供回滚接口。
设计分布式事务,本质上是在设计这些不确定性如何被系统吸收。
二、先厘清几个常被混在一起的概念
网上讲分布式事务,经常一上来就是 CAP、BASE、2PC、TCC、Seata。概念很多,但层次容易混。可以先按下面的方式拆开:

Seata 不是 TCC 的同义词,RocketMQ 事务消息也不是通用强一致事务。它们是实现工具,各自只解决一部分问题。
三、CAP、BASE 与分布式事务:别套公式
1. CAP 真正讨论的是什么
CAP 包含三个概念:
Consistency:一致性。在 CAP 语境中,通常指每次读取都能拿到最新写入或返回错误。
Availability:可用性。每个请求都能在有限时间内得到非错误响应,但数据不一定最新。
Partition Tolerance:分区容错。即使节点间通信中断或消息长时间延迟,系统仍能继续运行。
常见说法是“CAP 三选二”。这句话方便记忆,但不够准确。更实用的理解是:当网络分区真的发生时,系统必须在一致性和可用性之间做取舍。是拒绝部分请求,守住一致性;还是继续响应,接受短暂不一致。
2. CAP 的 C 不是 ACID 的 C
这两个 Consistency 经常被混为一谈。二者虽然名字相同,但并不是一个概念。
ACID 里的 Consistency 更关注事务执行前后是否满足业务约束,例如库存不能为负数,订单状态不能从
CANCELLED跳到PAID。CAP 里的 Consistency 更接近分布式读写的一致视图,例如任意节点读取时能否看到最新写入。
3. 刚性事务与柔性事务
工程上经常把方案分成两类:
刚性事务追求接近本地 ACID 的行为:
事务边界明确。
提交前资源通常会被锁定或保留。
任一参与者失败,整体回滚。
适合一致性要求高、事务时间短的场景。
典型方案:
XA。
2PC。
Seata XA。
某些数据库原生分布式事务。
柔性事务接受中间状态:
不要求所有参与者瞬间一致。
允许异步执行。
通过补偿、重试和对账达到最终一致。
更适合微服务和长业务流程。
典型方案:
TCC。
Saga。
Transactional Outbox。
本地消息表。
RocketMQ 事务消息。
最大努力通知。
需要注意:把刚性事务简单等同于 CP,把柔性事务简单等同于 AP,只能作为粗略直觉,不能当成严格推导。事务协议、复制一致性、服务可用性和网络分区下的行为,是不同维度的问题。
4. BASE 是什么
BASE 常用来描述最终一致系统的设计取向:
Basically Available:基本可用。
Soft State:软状态,允许存在中间状态。
Eventually Consistent:最终一致。
例如订单创建后先显示 PROCESSING,库存扣减和积分发放异步完成。短时间内各服务状态未完全同步,但系统能通过重试或补偿最终收敛。
这不是“数据错了也没关系”,而是“系统明确知道哪些中间状态允许存在,以及怎样恢复”。
四、用一张图建立分布式事务知识体系
可以先用这张结构图定位各类方案:

没有一种方案适合所有业务。下单、支付、退款、优惠券、积分、物流,可能分别使用不同策略。
五、XA 协议:数据库层面的分布式事务基础
XA 是 X/Open 提出的分布式事务处理规范。它定义了事务管理器和资源管理器如何协作。
两个核心角色:
Transaction Manager,简称 TM:事务管理器。负责开启全局事务,协调提交或回滚。
Resource Manager,简称 RM:资源管理器。通常是数据库、消息系统或其他事务资源。
调用关系可以理解为:

订单数据库和库存数据库都实现 XA 能力以后,TM 可以统一协调两个资源。
XA 是规范,不是 Java 专属技术。主流关系型数据库通常都提供不同程度的 XA 支持。
Java 里的 JTA 和 JTS
在 Java 生态里,经常会看到 JTA 和 JTS。
JTA:Java Transaction API,后来演进为 Jakarta Transactions API。它提供应用事务划分、事务管理器接口,以及对 X/Open XA 的 Java 映射。
JTS:Java Transaction Service。它是事务管理器实现相关规范。
可以粗略理解为:
XA:跨资源事务协议
Jakarta Transactions / JTA:Java 应用操作事务的标准接口
JTS:Java 事务服务实现规范业务开发里,不一定直接写 XA 接口,但理解它能帮助你判断框架底层在做什么。
六、两阶段提交 2PC:先准备,再决定
2PC,全称 Two-Phase Commit,是经典的两阶段提交协议。
参与角色:
Coordinator:协调者。
Participant:参与者。
整个过程分两步。

第一阶段:Prepare
协调者询问所有参与者:能不能提交?
Coordinator
-> order_db: PREPARE
-> stock_db: PREPARE参与者执行本地事务,但暂不真正提交。它们锁定必要资源,并向协调者回复:
YES:已经准备好,可以提交
NO:无法提交,需要回滚第二阶段:Commit 或 Rollback
如果所有参与者都返回 YES:
Coordinator
-> order_db: COMMIT
-> stock_db: COMMIT只要有一个参与者返回 NO 或超时:
Coordinator
-> order_db: ROLLBACK
-> stock_db: ROLLBACK2PC 的优点
语义清晰。
能协调多个支持事务协议的资源。
业务代码侵入相对小。
适合事务时间短、参与者可控、一致性要求高的场景。
2PC 的问题
1. 阻塞
参与者 prepare 成功后,需要等待协调者最终指令。等待期间资源可能被锁住。如果协调者故障,参与者无法随意决定提交还是回滚,只能等待恢复或超时处理。
2. 协调者故障
协调者是关键角色。它需要高可用和持久化事务状态,否则故障恢复时会很麻烦。
3. 网络异常带来的不确定状态
协调者发出 commit 后,如果部分参与者收到、部分参与者没收到,系统会进入复杂恢复流程。严格来说,成熟实现会依靠事务日志和恢复机制处理,而不是简单放任不一致。但恢复成本、锁持有时间和系统复杂度都是真实存在的。
4. 吞吐受限
参与者越多,网络往返越多;事务越长,锁持有越久。它不适合跨多个微服务执行长时间业务流程。
一个常见误区
2PC 不是只能用于“两个数据库”。它可以协调多个实现协议的事务资源。真正的限制是:参与资源必须提供对应事务能力,并且业务能够接受同步协调、锁资源和故障恢复成本。
对于第三方支付、物流公司接口、短信服务这类资源,通常没有办法让它们加入 XA 事务。
七、三阶段提交 3PC:理论上减轻阻塞,生产中少见
3PC 在 2PC 的基础上增加一个阶段:
CanCommit
-> PreCommit
-> DoCommit大致思路是:在真正提交前,让参与者状态更明确,并引入超时机制,减少协调者故障后无限等待的问题。但它依赖较强的网络时延假设。在真实系统中,网络延迟、分区、节点故障很难准确区分。超时以后自行提交,也可能引入新的不一致。因此,3PC 更适合理解分布式协议演进,不是微服务架构里的常规落地方案。
生产系统通常会通过复制状态机、共识协议、事务日志、补偿机制或工作流引擎处理故障恢复,而不是直接实现一套 3PC。
八、TCC:把事务控制权交给业务
TCC 是 Try、Confirm、Cancel 的缩写。它和 2PC 有相似的两阶段思想,但控制层次不同:
2PC 更偏资源层,常见于数据库。
TCC 发生在业务层,需要开发者实现预留、确认和取消逻辑。
还是以下单扣库存为例。
1. Try:检查并预留资源
库存服务先冻结库存,而不是直接扣减:
UPDATE stock
SET available = available - 1,
frozen = frozen + 1
WHERE sku_id = 1001
AND available >= 1;支付服务可以冻结余额:
UPDATE account
SET available_balance = available_balance - 100,
frozen_balance = frozen_balance + 100
WHERE user_id = 2001
AND available_balance >= 100;2. Confirm:确认执行
所有 Try 成功后,确认扣减冻结资源:
UPDATE stock
SET frozen = frozen - 1
WHERE sku_id = 1001
AND frozen >= 1;3. Cancel:取消预留
任一参与者 Try 失败,释放冻结库存:
UPDATE stock
SET available = available + 1,
frozen = frozen - 1
WHERE sku_id = 1001
AND frozen >= 1;TCC 的优点
锁粒度可以由业务控制。
不依赖数据库 XA。
跨数据库、跨服务都能落地。
资源预留后,本地数据库锁可以很快释放。
适合核心交易链路。
TCC 的缺点
侵入业务。
每个动作都要设计 Try、Confirm、Cancel。
需要处理幂等、空回滚、悬挂。
研发和测试成本高。
TCC 很强,但不是“给接口加三个方法”这么简单。它要求业务本身具备可冻结、可确认、可释放的资源模型。
TCC 最容易踩的三个坑
1. 空回滚
场景:
协调器调用 Try
-> 网络异常,请求实际没有到达库存服务
协调器判断失败
-> 调用 Cancel库存服务从未执行 Try,却收到了 Cancel。这就是空回滚。Cancel 必须允许这种情况,并安全返回。
常见做法是维护分支事务记录:
CREATE TABLE tcc_branch_record (
xid VARCHAR(128) NOT NULL,
branch_id VARCHAR(128) NOT NULL,
business_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
PRIMARY KEY (xid, branch_id)
);Cancel 时先查 Try 是否存在:
没有 Try 记录
-> 记录空回滚状态
-> 返回成功2. 幂等
Confirm 和 Cancel 都可能重试:
Confirm 已执行
-> 响应超时
-> 协调器再次调用 Confirm如果 Confirm 重复扣库存,系统就错了。
解决方法:
每个分支使用唯一
xid + branch_id。状态流转做条件更新。
重复请求读取当前状态后直接返回成功。
例如:
UPDATE tcc_branch_record
SET status = 'CONFIRMED'
WHERE xid = ?
AND branch_id = ?
AND status = 'TRIED';只有 TRIED 才能进入 CONFIRMED。
3. 悬挂
场景:
Try 请求发送后网络阻塞
协调器超时,先调用 Cancel
Cancel 执行完成
延迟的 Try 请求随后到达
Try 又冻结了资源此时已经没有后续 Confirm 或 Cancel,资源被永久挂住。Try 执行前必须检查:
当前分支是否已经 Cancel。
当前分支是否已经 Confirm。
是否存在空回滚标记。
如果二阶段已经发生,Try 不能再执行。这就是事务防悬挂。
九、Saga:更适合长流程的补偿事务
Saga 最早来自 Hector Garcia-Molina 和 Kenneth Salem 在 1987 年发表的论文。
它把一个长事务拆成多个本地事务:

每个本地事务都有对应补偿动作:

如果执行到 T3 失败:

Saga 的特点是:每个步骤完成后直接提交本地事务,不长时间锁资源。失败后通过业务补偿回到可接受状态。
一个订单 Saga
假设下单流程包含:
订单服务
支付服务
库存服务
物流服务正向流程:
创建订单
-> 扣款
-> 扣减库存
-> 创建物流单
-> 订单完成补偿流程:
物流创建失败
-> 恢复库存
-> 发起退款
-> 标记订单失败Saga 不追求数据库层面的回滚,而是执行业务意义上的反向动作。
退款就是一个很典型的补偿。钱扣掉以后,不可能让第三方支付系统“回滚事务日志”,只能再发起一笔退款交易。
Saga 的两种实现方式
1. Choreography:事件驱动
Choreography 可以理解为舞蹈编排。没有中央协调器,各服务监听事件并做出响应。
Order Service
-> 发布 ORDER_CREATED
Payment Service
-> 监听 ORDER_CREATED
-> 完成支付
-> 发布 PAYMENT_SUCCEEDED
Stock Service
-> 监听 PAYMENT_SUCCEEDED
-> 扣减库存
-> 发布 STOCK_DEDUCTED
Delivery Service
-> 监听 STOCK_DEDUCTED
-> 创建物流单
-> 发布 DELIVERY_CREATED优点:
服务解耦。
吞吐较高。
适合步骤少、链路清晰的流程。
缺点:
参与者多以后,很难看清完整流程。
容易出现事件环路。
异常补偿分散在多个服务里。
排查问题需要追踪很多消息。
如果只有两三个步骤,事件驱动很轻巧。如果十几个服务互相订阅,系统会慢慢变成一张很难解释的图。
2. Orchestration:协调器编排
Orchestration 可以理解为乐队指挥。一个 Saga Orchestrator 明确告诉各服务下一步做什么。
Order Saga Orchestrator
-> 创建订单
-> 命令 Payment Service 扣款
-> 命令 Stock Service 扣库存
-> 命令 Delivery Service 创建物流单
-> 命令 Order Service 标记完成出现异常:
Delivery Service 创建失败
-> Orchestrator 命令 Stock Service 恢复库存
-> Orchestrator 命令 Payment Service 退款
-> Orchestrator 命令 Order Service 标记失败优点:
流程集中,容易理解。
状态机清晰。
补偿顺序明确。
测试和运营干预方便。
缺点:
需要维护协调器。
协调器容易承载过多业务逻辑。
要认真设计状态持久化和故障恢复。
长流程、强运营、需要人工介入的业务,更适合编排式 Saga。Saga 的关键不是“回滚”,而是业务恢复。
十、Transactional Outbox:消息和业务数据一起落库
微服务里一个很常见的问题是:
更新数据库
发送 MQ 消息这两步怎么保证一致?
先更新数据库,再发消息:
数据库提交成功
-> 服务崩溃
-> 消息没发出去先发消息,再更新数据库:
消息发出
-> 数据库回滚
-> 下游消费了一个不存在的业务变化Transactional Outbox 的解决思路是:业务数据和待发送事件在同一个本地事务里写入数据库。
BEGIN;
INSERT INTO orders(order_id, user_id, status)
VALUES (90001, 2001, 'CREATED');
INSERT INTO outbox_event(
event_id,
aggregate_type,
aggregate_id,
event_type,
payload,
status,
created_at
)
VALUES (
'evt-001',
'ORDER',
'90001',
'ORDER_CREATED',
'{"orderId":90001,"userId":2001}',
'NEW',
NOW()
);
COMMIT;然后由独立发布器把 Outbox 事件发送到 MQ:
Outbox Relay
-> 扫描 NEW 事件
-> 发送 MQ
-> 标记 SENT或者直接订阅数据库 binlog,把 Outbox 事件推到消息系统。
Outbox 的优点
不需要 XA。
业务数据和事件在一个本地事务里提交。
实现直观。
能用于事件驱动和 Saga。
Outbox 的限制
发布器可能重复发送。
Outbox 表需要清理和归档。
轮询方案有延迟和数据库压力。
binlog 方案要维护 CDC 链路。
最重要的一点:Outbox 通常只能提供至少一次投递,因此消费者必须幂等。
本地消息表:Outbox 的常见落地方式
本地消息表和 Transactional Outbox 基本属于同一类思想:业务表和消息表在同一个数据库事务中写入。
一个简化表结构:
CREATE TABLE local_message (
message_id VARCHAR(64) PRIMARY KEY,
topic VARCHAR(128) NOT NULL,
business_key VARCHAR(128) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(32) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
next_retry_at DATETIME NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_topic_business_key(topic, business_key)
);Java 写法:
@Transactional
public Long createOrder(CreateOrderCommand command) {
Order order = orderRepository.save(Order.create(command));
localMessageRepository.save(
LocalMessage.newEvent(
UUID.randomUUID().toString(),
"ORDER_CREATED",
String.valueOf(order.getId()),
JsonUtils.toJson(OrderCreatedEvent.from(order))
)
);
return order.getId();
}后台任务发送:
public void publishPendingMessages() {
List<LocalMessage> messages = localMessageRepository.findReadyToSend(100);
for (LocalMessage message : messages) {
try {
messagePublisher.publish(message);
localMessageRepository.markSent(message.getMessageId());
} catch (Exception e) {
localMessageRepository.scheduleRetry(message.getMessageId());
}
}
}注意:即使 MQ 发送成功,markSent 也可能失败。下一次扫描时消息会再次发送。所以消费者必须幂等。
消费者幂等表
CREATE TABLE consumed_message (
consumer_group VARCHAR(128) NOT NULL,
message_id VARCHAR(64) NOT NULL,
consumed_at DATETIME NOT NULL,
PRIMARY KEY (consumer_group, message_id)
);消费时把“记录已消费”和“修改业务数据”放在一个本地事务里:
@Transactional
public void consume(OrderCreatedEvent event) {
if (consumedMessageRepository.exists("stock-consumer", event.messageId())) {
return;
}
stockRepository.deduct(event.skuId(), event.quantity());
consumedMessageRepository.save("stock-consumer", event.messageId());
}十一、RocketMQ 事务消息:把半消息和回查交给 MQ
本地消息表需要业务自己维护消息表、扫描任务、重试状态。RocketMQ 事务消息把其中一部分能力收进了 MQ。
核心流程:

流程说明:
如果 Producer 执行完本地事务,但二次确认因为网络异常丢失:

RocketMQ 官方文档明确指出,事务消息用于保证消息生产和本地事务之间的最终一致性。它不等于“消息消费方一定执行成功”,下游仍然需要消费重试和幂等。
RocketMQ 事务消息适合什么场景
本地核心事务成功后,可靠触发下游。
下游可以异步处理。
业务能实现本地事务状态回查。
可以接受最终一致。
典型场景:
支付成功后发放积分。
订单支付后清理购物车。
订单创建后触发异步库存处理。
状态变更后通知多个下游系统。
事务消息不解决什么
不保证消费者只消费一次。
不替代消费者幂等。
不提供跨服务同步回滚。
不适合必须立即拿到下游执行结果的业务。
十二、最大努力通知:该重试重试,该查询查询
最大努力通知适合通知类业务。
常见例子是支付回调:
支付平台
-> 第一次通知商户
-> 失败后重试
-> 按策略多次重试
-> 商户仍可主动查询支付结果它不是严格意义上的原子事务,而是通过:
多次通知。
指数退避。
状态查询接口。
对账任务。
尽最大努力让双方状态一致。
适合场景:
第三方支付回调。
短信、邮件、推送。
跨企业系统通知。
对实时性要求没那么高的同步。
对外部系统,最大努力通知往往比强行追求“同步成功”更现实。
十三、Seata:把多种分布式事务模式放进统一框架
Apache Seata 是常见的分布式事务中间件。根据官方文档,它提供四种事务模式:
AT。
XA。
TCC。
Saga。
Seata 里有三个核心角色:

调用关系:

1、Seata AT 模式:低侵入,但不是没有代价
Seata AT 模式适用于:
使用支持本地 ACID 事务的关系型数据库。
Java 应用通过 JDBC 访问数据库。
希望降低业务改造成本。
它可以理解为对两阶段事务的一种工程化演进。
第一阶段
业务 SQL 和回滚日志在同一个本地事务里提交:
解析业务 SQL
-> 保存修改前镜像 before image
-> 执行业务 SQL
-> 保存修改后镜像 after image
-> 写 undo_log
-> 获取全局锁
-> 提交本地事务
-> 释放本地锁和数据库连接第二阶段
全局提交:
异步清理 undo_log全局回滚:
根据 undo_log 生成反向补偿
-> 恢复业务数据为什么 AT 比传统 XA 更轻
AT 模式第一阶段提交本地事务后,就能较快释放本地数据库锁和连接。第二阶段提交可以异步完成。但它仍然要维护全局锁,避免多个全局事务对同一行产生脏写。
AT 模式的限制
依赖关系型数据库和 JDBC。
SQL 需要能被框架识别和代理。
维护
undo_log有成本。热点数据上的全局锁竞争仍会影响性能。
默认全局读隔离并不等同于强一致快照读。
跨第三方服务无法透明接入。
AT 模式适合改造成本敏感的内部服务,不适合拿来掩盖所有架构问题。
一个简化示例
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public Long createOrder(CreateOrderCommand command) {
stockClient.deduct(command.skuId(), command.quantity());
return orderRepository.save(Order.create(command)).getId();
}代码看起来轻量,但生产落地前仍要验证:
数据源代理是否生效。
undo_log是否创建。SQL 是否支持。
RPC 上下文是否传递 XID。
全局锁冲突怎么监控。
TC 是否高可用。
2、Seata XA 模式:标准 XA 语义
Seata XA 模式使用数据库对 XA 协议的支持管理分支事务。
执行阶段:
XA start
-> 执行业务 SQL
-> XA end
-> XA prepare完成阶段:
XA commit
或
XA rollback优点:
使用数据库原生 XA 能力。
语义更接近强一致事务。
数据无需由框架生成反向补偿 SQL。
缺点:
事务资源占用时间较长。
吞吐受限。
依赖数据库和驱动 XA 支持。
跨服务长事务不合适。
适合:
参与数据库数量有限。
事务短。
强一致优先。
系统能接受性能损耗。
3、Seata TCC 模式:核心交易链路的细粒度控制
Seata TCC 让业务定义 Try、Confirm、Cancel。
接口示意:
@LocalTCC
public interface StockTccAction {
@TwoPhaseBusinessAction(
name = "stockTccAction",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
boolean tryReserve(
BusinessActionContext context,
@BusinessActionContextParameter(paramName = "skuId") Long skuId,
@BusinessActionContextParameter(paramName = "quantity") Integer quantity
);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}Seata 官方文档强调,TCC 属于侵入式方案。它不依赖底层数据库事务模型来自动完成全部事情,而是把资源控制交给业务。
适合:
核心交易。
对性能要求高。
资源能冻结和释放。
团队能承担更高研发成本。
不适合:
业务没有可补偿模型。
参与方由外部公司维护。
流程长、步骤多。
团队还没有幂等、监控和压测基础。
4、Seata Saga 模式:状态机驱动的长事务
Seata Saga 面向长流程事务。每个参与者提交自己的本地事务,失败后补偿此前成功的参与者。
根据 Seata 官方文档,Saga 模式适合:
业务流程长。
参与者多。
包含第三方或遗留系统。
参与方无法提供 TCC 所需的三个接口。
Seata Saga 当前可以通过状态机引擎定义流程。一个节点代表一次服务调用,也可以配置对应补偿节点。
示意:
CreateOrder
-> DeductBalance
-> DeductStock
-> CreateDelivery
-> Succeed
失败时:
CreateDelivery 失败
-> CompensateStock
-> CompensateBalance
-> CancelOrder优点:
本地事务一阶段提交,不长时间锁资源。
流程可视化和可恢复。
适合异步、高吞吐、长链路。
缺点:
不天然保证隔离性。
补偿逻辑要由业务实现。
状态机设计和运营复杂度更高。
5、Seata 四种模式怎么选

选型要看业务:不要因为 AT 接入简单,就默认所有服务都上 AT。也不要因为 TCC 性能好,就让普通通知链路实现三套接口。
十四、常见业务该选哪种方案
1. 创建订单并扣减库存
可选:
内部系统、短链路、改造成本敏感:Seata AT。
核心秒杀、需要预留库存:TCC。
接受异步下单:Outbox + MQ + 库存消费者幂等。
2. 支付成功后发积分、发优惠券、清购物车
优先:
RocketMQ 事务消息。
Outbox + MQ。
这些下游动作不应该阻塞支付主流程。失败后重试即可。
3. 退款流程
优先:
Saga。
状态机 + 补偿 + 对账。
退款涉及支付渠道、库存、优惠券、积分、订单状态,流程长,而且第三方支付不可能加入本地事务。
4. 跨两个内部数据库的短事务
可选:
XA。
Seata XA。
如果能改模型,优先考虑合库或异步解耦。
分布式事务有成本。如果两个表本来就应该处于同一个一致性边界,先问一句:是不是服务拆得过细?
5. 支付平台通知商户
优先:
最大努力通知。
商户查询接口。
定时对账。
外部系统无法完全控制,重试和查询接口比强行同步更可靠。
方案选型速查表

结语:分布式事务的答案通常不是“全部回滚”
单机事务给人的感觉很干净:成功就是成功,失败就是失败。
分布式系统没有这么听话。网络超时、服务重启、消息重试、第三方回调、人工处理,都会让业务处于中间状态。所以,设计分布式事务时,不要只问:
怎么保证所有操作一起提交?
还要问:
如果无法一起提交,系统怎么恢复?
如果状态不明确,系统怎么查询?
如果消息重复,系统怎么幂等?
如果自动补偿失败,系统怎么对账?
如果流程卡住,运营怎么介入?
XA、2PC、TCC、Saga、Outbox、RocketMQ 事务消息、Seata 都只是工具。真正可靠的系统,靠的是清楚的业务边界、有限状态机、幂等写入、可控重试、补偿流程和持续对账。
在分布式事务里,最成熟的设计往往不是“永远不出错”,而是出了错以后,系统知道自己在哪里,也知道下一步该怎么走。
评论区