专栏《Java高级进阶之路》从CRUD到AI工程师的完整跃迁路径Day 25/ 90主题XA/DTP模型/TCC/SAGA/AT五种方案对比Seata AT模式源码关键路径分析在单体拆分微服务很容易掉进一个坑本地事务的ACID在跨服务调用时彻底失效。你以为加了Transactional就万事大吉其实它只保护了单个数据库连接根本管不了隔壁服务的库。分布式事务不是高级技术而是必修课。对比五种主流方案XA、TCC、SAGA、AT、本地消息表将重点拆解Seata AT模式源码关键路径让你既能选型、也能排坑。一、先搞清楚问题为什么本地事务不够用下单业务经典调用链订单服务(创建订单) → 支付服务(扣款) → 库存服务(扣减) → 物流服务(创建运单)每个服务都有自己的数据库。假设订单库和库存库都在MySQL 5.7事务流程是// 订单服务伪代码 Transactional public void createOrder(OrderDTO dto) { // 1. 本地事务插入订单 orderMapper.insert(dto); // 2. 远程调用扣库存 inventoryClient.deduct(dto.getSkuId(), dto.getCount()); // 3. 远程调用扣款 payClient.charge(dto.getUserId(), dto.getAmount()); }问题来了第2步远程调用超时你不知道库存服务到底扣没扣第3步支付成功但事务回滚钱退不退网络分区、节点宕机、消息丢失——本地事务的原子性在这里全是空气这就是分布式事务要解决的核心问题让一组跨服务、跨数据库的操作要么全部成功要么全部失败且对故障具备一定容忍能力。二、五大方案横向对比在写代码之前先看一张决策矩阵避免一上来就选错方案方案一致性性能侵入性适用场景XA二阶段强一致差同步阻塞低数据库原生短事务、低并发、强一致场景TCC强一致中Try预留资源高需业务改造金融核心、电商下单SAGA最终一致好异步中需补偿逻辑长事务、跨多服务编排AT最终一致好近乎无侵入低注解代理大多数业务首选本地消息表最终一致中异步轮询中需消息表跨服务异步通知经验新项目无脑上Seata AT金融核心用TCC长事务编排用SAGA。XA现在基本只在银行传统核心系统见得多微服务场景越来越少。三、方案1XA二阶段提交教科书但生产慎用XA是X/Open DTP模型的标准实现MySQL从5.0开始原生支持。原理是两阶段阶段一Prepare事务管理器问所有参与者能不能提交阶段二Commit/Rollback所有参与者都OK才提交否则全部回滚。/** * XA二阶段提交示例基于Atomikos * 依赖com.atomikos:transactions-jdbc:6.0.0 */ public class XaDemo { public static void main(String[] args) throws Exception { // 1. 配置两个数据源order、inventory UserTransactionManager txManager new UserTransactionManager(); txManager.setTransactionTimeout(60); txManager.init(); // 2. 开启XA事务 UserTransaction utx new UserTransactionImp(); utx.setTransactionTimeout(60); utx.begin(); // 阶段一准备 try (Connection orderConn orderDataSource.getConnection(); Connection invConn inventoryDataSource.getConnection()) { // 订单库插入 PreparedStatement orderPs orderConn.prepareStatement( INSERT INTO t_order(user_id, amount) VALUES(?, ?)); orderPs.setLong(1, 1001L); orderPs.setBigDecimal(2, new BigDecimal(99.00)); orderPs.executeUpdate(); // 库存库扣减 PreparedStatement invPs invConn.prepareStatement( UPDATE t_stock SET count count - ? WHERE sku_id ?); invPs.setInt(1, 1); invPs.setString(2, SKU001); invPs.executeUpdate(); // 阶段二提交 utx.commit(); } catch (Exception e) { utx.rollback(); // 任一失败全部回滚 throw e; } } }XA的致命问题同步阻塞Prepare阶段锁住资源整个过程所有相关行都被锁协调者单点TM宕机后参与者会一直锁住资源被锁死性能差TPS在数据量大时断崖式下跌所以生产环境我几乎没用过XA。知道原理就行别真用。四、方案2Seata AT模式生产首选AT模式的核心思想是自动补偿——业务代码几乎不用改Seata通过分析SQL自动生成回滚日志UNDO_LOG失败时反向补偿。架构三角色TC (Transaction Coordinator) - 事务协调器独立部署 TM (Transaction Manager) - 事务管理器GlobalTransactional注解入口 RM (Resource Manager) - 资源管理器每个微服务一个先看业务代码和本地事务几乎一样/** * Seata AT模式业务代码 * 依赖spring-cloud-starter-alibaba-seata seata-all 1.6 */ Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private InventoryClient inventoryClient; // Feign远程调用 Autowired private PayClient payClient; /** * GlobalTransactional 开启全局事务 * 这个注解会拦截方法生成XID全局事务ID并传播到下游服务 */ GlobalTransactional(name create-order-tx, rollbackFor Exception.class) public OrderResult createOrder(OrderDTO dto) { // 1. 本地插入订单被Seata代理注册UNDO_LOG Order order new Order(); order.setUserId(dto.getUserId()); order.setAmount(dto.getAmount()); orderMapper.insert(order); // 2. 远程扣库存XID通过请求头自动传递 inventoryClient.deduct(dto.getSkuId(), dto.getCount()); // 3. 远程扣款 payClient.charge(dto.getUserId(), dto.getAmount()); return OrderResult.success(order.getId()); } }业务侧代码确实简洁——加个注解就行。但我们要理解背后原理。Seata AT模式的两阶段原理图解关键源码路径Seata 1.6核心类/** * Seata AT模式核心DataSourceProxy * 路径io.seata.rm.datasource.DataSourceProxy * 作用包装原始DataSource拦截SQL生成UNDO_LOG */ public class DataSourceProxy extends AbstractDataSourceProxy { Override public Connection getConnection() throws SQLException { Connection targetConnection targetDataSource.getConnection(); // 返回代理连接关键在这一步 return new ConnectionProxy(targetConnection, this, resourceId); } } /** * 关键方法processGlobalTransactionCommit * 路径ConnectionProxy.java * * 阶段一执行流程 * 1. SQLExecutor.execute() 解析业务SQL * 2. 前镜像查询SELECT * FROM stock WHERE id ? (FOR UPDATE) * 3. 执行业务SQL * 4. 后镜像查询SELECT * FROM stock WHERE id ? * 5. 生成UNDO_LOG写入数据库 * 6. 提交本地事务 * * 阶段二回滚流程 * 1. UndoLogManager.undo() 读取UNDO_LOG * 2. 数据校验当前数据 after_img ? 否则人工介入 * 3. 反向SQLUPDATE stock SET count before_img.count WHERE id ? AND count after_img.count */UNO_LOG表结构每个业务库都要建生产环境踩坑点AT模式的脏写问题阶段一提交后释放行锁期间其他事务可能修改数据导致回滚时数据校验失败。解决开启SELECT FOR UPDATE但会牺牲性能。UNDO_LOG表清理长事务会导致UNDO_LOG膨胀需要定期清理log_status1且超过7天的数据。TC高可用单TC节点挂了全完蛋必须用Nacos或File/DB模式部署TC集群。五、方案3TCC模式金融核心用TCC是Try-Confirm-Cancel三阶段业务侵入性强但性能好、一致性强。代码示例/** * TCC模式库存服务 * 三个方法都要实现try预留、confirm确认、cancel取消 * 注解来自org.mengyun.tcctransaction.* 或 seata TwoPhaseBusinessAction */ Service public class InventoryTccService { Autowired private InventoryMapper inventoryMapper; Autowired private FrozenStockMapper frozenMapper; // 冻结库存表 /** * Try预留资源。冻结库存不直接扣减 */ TwoPhaseBusinessAction(name deductInventory, commitMethod confirm, rollbackMethod cancel) public boolean tryDeduct(BusinessActionContext ctx, String skuId, int count) { // 1. 冻结库存到独立表 FrozenStock frozen new FrozenStock(); frozen.setSkuId(skuId); frozen.setFrozenCount(count); frozen.setXid(ctx.getXid()); frozenMapper.insert(frozen); // 2. 原表扣减 int rows inventoryMapper.freezeStock(skuId, count); return rows 0; } /** * Confirm确认扣减Try成功后才调用 */ public boolean confirm(BusinessActionContext ctx) { // 删除冻结记录物理扣减完成 frozenMapper.deleteByXid(ctx.getXid()); return true; } /** * Cancel取消预留Try失败或全局回滚时调用 */ public boolean cancel(BusinessActionContext ctx) { // 释放冻结库存 inventoryMapper.unfreezeStock(ctx.getSkuId(), /*count*/); frozenMapper.deleteByXid(ctx.getXid()); return true; } }TCC的两个细节空回滚Try请求未到达Cancel却先到了 → Cancel要做幂等判断悬挂Cancel执行完Try又到了 → Try也要做幂等检测到已Cancel直接返回失败这两个坑是TCC代码审计必查项。六、建议老梁干了20年见过太多团队栽跟头。给你三条不会写在官方文档里的经验1. 不要过度设计新项目用Seata AT 本地消息表覆盖90%场景。上来就搞TCC、SAGA的90%是在给自己挖坑。2. 永远要设计幂等不管用哪种方案远程调用必须幂等。Redis SETNX 唯一业务ID或者数据库唯一索引兜底。分布式事务不是银弹业务幂等才是最后一道防线。3. 监控三件套必须上TC事务状态监控Seata Dashboard或自研UNDO_LOG表大小监控超过阈值告警业务侧XID全链路日志用MDC透传方便定位分布式事务没有银弹每种方案都是一致性-性能-复杂度三角的取舍。Seata AT是当前最平衡的选择但TCC在金融场景依然不可替代。记住一句话能不用分布式事务就不用本地消息表能解决的就别上框架。下篇预告Day26 我们进入Spring AI世界5行代码接入大模型——从环境准备到ChatClient第一个demo再到通义千问/DeepSeek的真实接入配置零基础也能跟上。下期我们继续拆开揉碎讲AI工程化。