Seata 分布式事务落地——AT 模式与 TCC 模式的生产选型指南
一、分布式事务的本质问题
在微服务架构中,一个业务操作往往需要跨多个服务、多个数据库完成。以电商下单为例,流程涉及订单服务(创建订单)、库存服务(扣减库存)、账户服务(扣减余额)。这三个操作要么全部成功,要么全部回滚——这正是分布式事务需要解决的问题。
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案,在 Java 生态中应用最为广泛。它提供了 AT、TCC、Saga、XA 四种模式,其中 AT 模式和 TCC 模式是生产环境使用最多的两种。本文将从原理、代码、生产经验三个维度对比这两种模式。
flowchart TD subgraph "Seata 核心组件" TC["TC - 事务协调器<br/>Transaction Coordinator<br/>(Seata Server)"] TM["TM - 事务管理器<br/>Transaction Manager<br/>(@GlobalTransactional 标注方)"] RM["RM - 资源管理器<br/>Resource Manager<br/>(各微服务的数据库连接)"] end subgraph "下单业务流程" A["订单服务<br/>OrderService<br/>(TM角色)"] -->|"分支事务1"| B["库存服务<br/>InventoryService<br/>(RM角色)"] A -->|"分支事务2"| C["账户服务<br/>AccountService<br/>(RM角色)"] end TM -->|"注册/提交/回滚全局事务"| TC B -->|"注册/报告分支事务状态"| TC C -->|"注册/报告分支事务状态"| TC subgraph "AT模式两阶段" D1["一阶段:执行业务SQL + 记录undo_log"] --> D2["二阶段:异步删除undo_log(提交)"] D1 --> D3["二阶段:根据undo_log回滚数据(回滚)"] end subgraph "TCC模式两阶段" E1["Try:资源预留/检查"] --> E2["Confirm:提交业务操作"] E1 --> E3["Cancel:释放预留资源"] end二、AT 模式:自动补偿的无侵入方案
2.1 原理简介
AT 模式的核心思路是基于数据库的自动补偿。它不需要业务代码做任何改造,只需在数据库中增加一张undo_log表。执行流程分为两阶段:
- 一阶段:执行业务 SQL,同时将修改前后的数据(前镜像/后镜像)记录到
undo_log表,然后提交本地事务。 - 二阶段:如果全局事务提交,异步删除
undo_log;如果全局事务回滚,根据undo_log中的前镜像生成反向 SQL 进行补偿。
2.2 工程配置
# application.yml seata: tx-service-group: my_test_tx_group # 事务分组名称 service: vgroup-mapping: my_test_tx_group: default # 映射到 Seata Server 集群 grouplist: default: 127.0.0.1:8091 # Seata Server 地址 config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP registry: type: nacos nacos: application: seata-server server-addr: 127.0.0.1:88482.3 AT 模式代码示例
import io.seata.spring.annotation.GlobalTransactional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 订单服务——分布式事务发起方(TM 角色)。 * 使用 @GlobalTransactional 注解开启全局事务。 */ @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private InventoryFeignClient inventoryClient; @Autowired private AccountFeignClient accountClient; /** * 创建订单——全局事务入口。 * AT 模式只需要在业务入口方法加 @GlobalTransactional, * 各子服务的本地事务由 Seata 代理自动管理。 * * @param userId 用户ID * @param productId 商品ID * @param quantity 购买数量 * @return 订单ID * @throws OrderCreateException 如果下单失败 */ @GlobalTransactional(name = "create-order", timeoutMills = 300000, rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, Long productId, Integer quantity) throws OrderCreateException { // 参数校验 if (userId == null || productId == null || quantity == null || quantity <= 0) { throw new IllegalArgumentException("订单参数不合法: userId=" + userId + ", productId=" + productId + ", quantity=" + quantity); } // 1. 创建订单(本地事务,被 Seata 代理管理) Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setQuantity(quantity); order.setStatus(OrderStatus.CREATED); orderMapper.insert(order); // 2. 扣减库存(远程调用,自动加入全局事务分支) try { inventoryClient.deduct(productId, quantity); } catch (Exception e) { throw new OrderCreateException("扣减库存失败: " + e.getMessage(), e); } // 3. 扣减余额(远程调用,自动加入全局事务分支) try { accountClient.deduct(userId, order.getTotalAmount()); } catch (Exception e) { throw new OrderCreateException("扣减余额失败: " + e.getMessage(), e); } // 4. 更新订单状态 order.setStatus(OrderStatus.PAID); orderMapper.updateById(order); return order.getId(); } }import io.seata.core.context.RootContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; /** * 库存服务——分布式事务参与者(RM 角色)。 * AT 模式下,本地 @Transactional 的事务由 Seata 代理接管。 */ @RestController @RequestMapping("/inventory") public class InventoryController { @Autowired private InventoryMapper inventoryMapper; /** * 扣减库存接口。 * Seata AT 代理会自动在本地事务提交前记录 undo_log。 * * @param productId 商品ID * @param quantity 扣减数量 * @throws InventoryException 如果库存不足 */ @PostMapping("/deduct") @Transactional(rollbackFor = Exception.class) public void deduct(Long productId, Integer quantity) throws InventoryException { // 打印全局事务 XID,用于日志追踪 System.out.println("全局事务 XID = " + RootContext.getXID()); // 查询当前库存 Inventory inventory = inventoryMapper.selectForUpdate(productId); if (inventory == null) { throw new InventoryException("商品不存在: productId=" + productId); } if (inventory.getStock() < quantity) { throw new InventoryException("库存不足: 当前库存=" + inventory.getStock() + ", 需要=" + quantity); } // 扣减库存(Seata 会自动记录前镜像和后镜像到 undo_log) int affected = inventoryMapper.deductStock(productId, quantity); if (affected != 1) { throw new InventoryException("库存扣减失败,影响行数异常: " + affected); } } }2.4 AT 模式的局限性
- 仅支持关系型数据库:
undo_log依赖数据库事务,Redis/MongoDB 等 NoSQL 不适用。 - 性能开销:每次写操作都需要额外记录前后镜像数据。
- 脏写风险:默认隔离级别为"读未提交",高并发下可能出现脏读。可通过
@GlobalLock或SELECT FOR UPDATE缓解。
三、TCC 模式:业务侵入式的资源预留方案
3.1 原理简介
TCC(Try-Confirm-Cancel)模式要求开发者自行实现三个方法:
- Try:资源检查和预留(如冻结库存、预占额度)。
- Confirm:提交业务操作(如真正扣减冻结的库存)。
- Cancel:释放预留的资源(如解冻库存)。
TCC 模式对代码有侵入性,但换来的是更好的性能(无全局锁)和更广的适用范围(Redis、消息队列等均可参与)。
3.2 TCC 模式代码示例
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; /** * 库存服务的 TCC 接口定义。 * 使用 @LocalTCC 注解标记为 TCC 服务。 */ @LocalTCC public interface InventoryTccAction { /** * Try 阶段:预扣库存(将库存从 available 转移到 frozen)。 * * @param actionContext 事务上下文(Seata 自动注入) * @param productId 商品ID * @param quantity 预扣数量 * @return true 表示预留成功 */ @TwoPhaseBusinessAction(name = "inventoryTcc", commitMethod = "commit", rollbackMethod = "rollback") boolean prepareDeduct(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "productId") Long productId, @BusinessActionContextParameter(paramName = "quantity") Integer quantity); /** * Confirm 阶段:确认扣减(将库存从 frozen 中真正扣除)。 * * @param actionContext 事务上下文 * @return true 表示确认成功 */ boolean commit(BusinessActionContext actionContext); /** * Cancel 阶段:回滚(将库存从 frozen 恢复到 available)。 * * @param actionContext 事务上下文 * @return true 表示回滚成功 */ boolean rollback(BusinessActionContext actionContext); }import io.seata.rm.tcc.api.BusinessActionContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 库存 TCC 接口实现。 * * 关键设计原则: * - Try 阶段做资源预留,不真正扣减 * - Confirm 阶段必须幂等 * - Cancel 阶段必须幂等且允许空回滚 */ @Service public class InventoryTccActionImpl implements InventoryTccAction { @Autowired private InventoryMapper inventoryMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean prepareDeduct(BusinessActionContext actionContext, Long productId, Integer quantity) { // 空回滚检测:如果这条记录之前已经被 Cancel,则不再执行 Try String xid = actionContext.getXid(); if (inventoryMapper.existsTccLog(xid)) { System.out.println("检测到空回滚,跳过 Try: xid=" + xid); return true; } // 扣减可用库存,增加冻结库存(Seata 代理管理本地事务) int affected = inventoryMapper.freezeStock(productId, quantity); if (affected != 1) { throw new RuntimeException("冻结库存失败: productId=" + productId + ", quantity=" + quantity); } // 记录 TCC 操作日志(用于幂等性判断和故障恢复) inventoryMapper.insertTccLog(xid, "INVENTORY_DEDUCT", productId.toString(), quantity.toString(), "TRY"); return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean commit(BusinessActionContext actionContext) { String xid = actionContext.getXid(); // 幂等性检查:如果已 Confirm,则直接返回成功 if (inventoryMapper.isTccPhaseCompleted(xid, "CONFIRM")) { return true; } Long productId = Long.valueOf(actionContext.getActionContext("productId").toString()); Integer quantity = Integer.valueOf(actionContext.getActionContext("quantity").toString()); // 真正扣减冻结库存 inventoryMapper.confirmDeduct(productId, quantity); // 更新 TCC 日志状态 inventoryMapper.updateTccLogStatus(xid, "CONFIRM"); return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean rollback(BusinessActionContext actionContext) { String xid = actionContext.getXid(); // 幂等性检查:如果已 Cancel,则直接返回成功 if (inventoryMapper.isTccPhaseCompleted(xid, "CANCEL")) { return true; } Long productId = Long.valueOf(actionContext.getActionContext("productId").toString()); Integer quantity = Integer.valueOf(actionContext.getActionContext("quantity").toString()); // 解冻库存(恢复可用库存) inventoryMapper.unfreezeStock(productId, quantity); // 记录回滚日志(防止空回滚后的 Try 再次执行) inventoryMapper.insertTccLog(xid, "INVENTORY_DEDUCT", productId.toString(), quantity.toString(), "CANCEL"); return true; } }四、AT vs TCC:生产选型决策矩阵
| 维度 | AT 模式 | TCC 模式 |
|---|---|---|
| 代码侵入性 | 无侵入,只需配置 | 高侵入,需实现 Try/Confirm/Cancel |
| 性能 | 中等(需记录 undo_log) | 较好(无全局锁) |
| 适用场景 | 关系型数据库场景 | 任何资源类型(DB/Redis/MQ) |
| 幂等性 | 框架自动保证 | 开发者自行保证 |
| 空回滚/悬挂 | 框架自动处理 | 开发者自行处理 |
| 隔离性 | 默认读未提交 | 可自定义 |
| 运维复杂度 | 低 | 中(需管理业务补偿表) |
| 开发效率 | 高(1-2天集成) | 中(3-5天开发) |
选型建议:
- 纯关系型数据库 + 改造时间紧 → 优先选AT 模式。
- 涉及 Redis/MQ 等非数据库资源 → 必须选TCC 模式。
- 高并发库存扣减场景 → 推荐TCC 模式(避免热点行锁竞争)。
- 简单业务 + 低并发 →AT 模式性价比最高。
五、总结
Seata 的 AT 模式和 TCC 模式各有所长,没有绝对的优劣之分。在实际项目中,我们通常在一个应用中混用两种模式:核心交易链路(订单/支付)使用 TCC 以保证性能和资源灵活性,非核心链路(日志记录/通知)使用 AT 以降低开发成本。
无论选择哪种模式,以下三条生产铁律不可忽视:
- 超时机制:全局事务必须设置
timeoutMills,超时后自动触发回滚。生产环境建议设置在 30-60 秒之间——过短可能误杀正常执行的长事务,过长则降低故障切换速度。 - 幂等设计:TCC 模式的 Confirm/Cancel 必须幂等;AT 模式虽由框架保证,但建议在业务代码中也加入防重逻辑(如基于 XID 的事务去重表)。
- 监控告警:监控
undo_log堆积量、TCC 事务超时率、全局事务平均耗时等关键指标。undo_log 堆积(超过 10000 行或超过 24 小时未被清理)意味着大量历史数据占用存储,需排查 Seata Server 的二阶段调度是否正常。
作者:程序员鸭梨(李然),Java 架构师,专注微服务架构与分布式事务实践。欢迎留言交流。