Seata 分布式事务是微服务架构下解决跨库、跨服务数据一致性的主流方案。当一笔下单请求要同时扣减库存、创建订单、扣减账户余额,任何一个环节失败都必须让其他操作回滚,否则就会出现”少扣钱多发货”的资金漏洞。本文从 Seata 的 AT 自动补偿模式讲起,逐步延伸到 TCC 手动编排,帮你在大事务面前既能保住一致性,又不至于拖垮系统性能。
一、为什么单机事务撑不住分布式场景
在单库单表里,我们靠数据库自带的 ACID 和 @Transactional 就能保证”要么全成、要么全不做”。但微服务的核心就是把一个单体拆成多个独立部署的服务,每个服务拥有自己的数据库。当你在一笔业务里既要调库存服务、又要调订单服务、还要调账户服务,这三个操作落在三个不同的库里,本地事务就管不了全局了。
业界通常用 CAP 定理来权衡:分区容错在分布式下必选,于是只能在一致性(C)和可用性(A)之间取舍。强一致意味着要引入分布式事务协调者,付出一定的延迟代价;最终一致则把回滚改写成”补偿”,用异步对账兜底。Seata 的 AT 模式走的是”强一致加自动补偿”的折中路线,而 TCC 把补偿的编写权交回给业务,灵活但更重。
关于本地事务的边界与传播行为,建议先吃透 Spring 事务传播机制;当服务间调用走 gRPC 时,事务上下文的传递又多一层复杂度(参考 gRPC 微服务通信实战)。
二、Seata 的四大角色与整体架构
Seata 把分布式事务拆成几个核心角色:
- TC(Transaction Coordinator):事务协调者,独立部署的服务,负责维护全局事务和分支事务的状态,驱动提交或回滚。
- TM(Transaction Manager):事务管理器,嵌在发起方应用里,负责定义全局事务的边界(@GlobalTransactional)。
- RM(Resource Manager):资源管理器,嵌在每个参与者应用里,管理分支事务与 TC 的注册、上报。
- Undo Log:AT 模式下的关键设施,记录数据修改前镜像,用于自动回滚。
一次典型的全局事务流程是:TM 向 TC 申请开启全局事务拿到 XID,XID 通过上下文(HTTP Header 或 RPC 透传)传播到各 RM;每个 RM 执行业务 SQL 时向 TC 注册分支事务,并在本地落一份 undo_log;全部成功后 TC 通知各 RM 提交(异步清理 undo_log),任一失败则通知回滚(用 undo_log 反向补偿)。
三、AT 模式:无侵入的自动补偿
AT 模式是 Seata 最受欢迎的模式,最大特点是对业务代码零侵入:你只需要加一个 @GlobalTransactional 注解,Seata 就会在底层自动完成”解析 SQL、生成前置镜像、加全局锁、注册分支、异常时回滚”的全过程。
它依赖一张 undo_log 表,这张表必须建在每个参与分布式事务的业务的数据库里。事务提交前,Seata 把”修改前的数据快照”写进 undo_log;如果全局事务需要回滚,就用快照生成反向 SQL 把数据改回去,再删除快照。
-- 每个业务库都要建 undo_log 表
CREATE TABLE undo_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB;
四、AT 模式代码落地(Spring Boot)
先在配置文件里声明事务分组,并指向 TC 的地址。Seata 1.5 之后默认用注册中心,这里以直连为例:
# application.yml
seata:
enabled: true
tx-service-group: my_tx_group # 事务分组
service:
vgroup-mapping:
my_tx_group: default # 映射到 TC 集群
grouplist:
default: 127.0.0.1:8091 # TC 地址
registry:
type: file # 简单起见用 file,生产用 nacos
业务侧只需在发起方法上标注 @GlobalTransactional,其余分支方法照常用本地 @Transactional 即可。Seata 会自动代理数据源,拦截 SQL:
@Service
public class OrderService {
@Autowired private InventoryMapper inventoryMapper;
@Autowired private AccountMapper accountMapper;
@Autowired private OrderMapper orderMapper;
// 只加一个注解,三个库的写操作被纳入同一全局事务
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Long userId, Long itemId, int count) {
inventoryMapper.deduct(itemId, count); // 库存库
accountMapper.deduct(userId, count * 10); // 账户库
orderMapper.insert(userId, itemId, count);// 订单库
// 任意一步抛异常,AT 会用 undo_log 自动回滚前面两步
}
}
值得注意的是,AT 模式用”全局锁”防止脏写:在分支事务提交前,它会锁住涉及数据行的主键范围,直到全局事务结束才释放。这意味着两个并发的全局事务不能同时改同一行。当你的下单链路涉及分库分表时(例如用 分库分表 ShardingSphere 做水平拆分),AT 会把全局锁按实际路由的库分别加锁,配置上要额外留意。
五、AT 模式的局限与坑
AT 虽好,但有清晰的能力边界:
- 热点行瓶颈:全局锁串行化使得高并发扣减同一行(如爆款商品库存)会严重排队,此时 AT 反而成为瓶颈。
- 仅支持关系型:undo_log 机制依赖 SQL 解析,只对 MySQL、PostgreSQL、Oracle 等关系型数据库生效,Redis、MongoDB 等不行。
- 隔离级别:AT 默认是”读未提交”的本地隔离,跨全局事务的脏读需要靠全局锁或人工 SELECT FOR UPDATE 解决。
- 大事务 undo_log 膨胀:一次改成千上万行的 SQL,undo_log 也会跟着膨胀,回滚成本高。
如果你的场景是热点库存扣减、或需要跨非关系型存储,就该考虑 TCC 了。另外,数据库本身的高可用(主从复制、读写分离,见 MySQL 主从复制与读写分离)是分布式事务能稳定运行的地基,别指望 Seata 替你兜住底层存储的故障。
六、TCC 模式:把补偿权交回业务
TCC(Try-Confirm-Cancel)把每个参与者拆成三个方法:Try 预留资源(如冻结库存而非直接扣减)、Confirm 真正提交(把冻结转为扣减)、Cancel 释放预留(把冻结退回)。它不依赖 undo_log,而是靠业务自己写”反向操作”,因此能跨任意存储、且性能更高,代价是代码侵入重、要保证幂等。
// 1) 定义 TCC 接口,用 Seata 的注解声明三阶段
@LocalTCC
public interface InventoryTccAction {
@TwoPhaseBusinessAction(name = "deductInventory",
commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(BusinessActionContext ctx, Long itemId, int count);
boolean commit(BusinessActionContext ctx); // Confirm
boolean rollback(BusinessActionContext ctx); // Cancel
}
// 2) 实现:Try 冻结、Confirm 扣减、Cancel 解冻
@Component
public class InventoryTccActionImpl implements InventoryTccAction {
@Override
public boolean prepare(BusinessActionContext ctx, Long itemId, int count) {
return inventoryMapper.freeze(itemId, count) > 0; // 冻结而非扣减
}
@Override
public boolean commit(BusinessActionContext ctx) {
Long itemId = ctx.getActionContext("itemId", Long.class);
int count = ctx.getActionContext("count", Integer.class);
return inventoryMapper.confirmDeduct(itemId, count) > 0;
}
@Override
public boolean rollback(BusinessActionContext ctx) {
Long itemId = ctx.getActionContext("itemId", Long.class);
int count = ctx.getActionContext("count", Integer.class);
return inventoryMapper.unfreeze(itemId, count) > 0; // 解冻
}
}
TCC 的成败关键在于幂等:网络抖动可能导致 Confirm 或 Cancel 被重复调用,因此每一阶段都要能安全地重复执行(通常用”已处理状态位”去重)。这点和本地事务传播里的嵌套回滚思路不同,建议结合 Spring 事务传播机制 对比理解”谁该回滚、回滚到哪”。
七、AT 还是 TCC?一表说清
选型时不必二选一,常见做法是”AT 兜底常规链路、TCC 攻坚热点”。下面这张对照表覆盖大多数决策点:
| 维度 | AT 模式 | TCC 模式 |
|---|---|---|
| 代码侵入 | 几乎无(一个注解) | 重(三方法加幂等) |
| 性能 | 受全局锁约束 | 高,无全局锁 |
| 存储支持 | 仅关系型 | 任意(含 Redis、MQ) |
| 一致性 | 强一致 | 强一致(需自保证) |
| 适用场景 | 常规跨库写 | 热点、跨存储、高性能 |
八、生产落地清单
无论选哪种模式,上线前都建议过一遍这张检查表,避免半夜被一致性告警叫醒:
| 检查项 | 要点 |
|---|---|
| TC 高可用 | TC 必须集群部署,否则它挂了全局事务全卡死 |
| undo_log 表 | 每个业务库都建,且和主表在同一库(同库事务) |
| XID 透传 | Feign、gRPC、Dubbo 都要配置拦截器把 XID 往下传 |
| 幂等 | TCC 的 Confirm、Cancel 必须可重入 |
| 空回滚与防悬挂 | 处理 Try 未执行但 Cancel 先到的极端时序 |
微服务间的事务协调建立在稳定服务治理之上,先把 Spring Cloud 微服务治理 的注册发现与熔断限流做扎实,Seata 才能安心工作;若链路里用到了异步线程,注意 Java 21 虚拟线程 的线程模型可能影响 XID 的上下文传递。
九、小结
Seata 分布式事务用 AT 模式把”跨库回滚”变得几乎无侵入,又用 TCC 模式给热点与跨存储场景留出高性能通道。记住三件事:AT 靠 undo_log 与全局锁自动补偿但有热点瓶颈;TCC 把补偿交回业务、要自己保证幂等;生产环境务必保证 TC 高可用与 XID 透传。把一致性边界想清楚,分布式事务就不再是让人失眠的难题。




