说实话看到标题里同时挂着“Spring Boot 4 Java 25”和“Seata”我第一反应是这回又要趟一遍分布式事务的浑水了。去年我在某电商系统里把一套拆成订单、库存、支付三个服务的项目硬生生塞进了分布式事务的坑里前前后后踩了不少雷。最近刚好把技术栈升到 Spring Boot 4 和 Java 25又把 Seata 的 AT / TCC / Saga / XA 四种模式挨个试了一遍和项目里原本就在用的 Spring JDBC 事务做了对照才算是把“什么时候用哪种事务”这件事彻底理顺了。这篇内容不是给你抄配置的而是想把我在选型和落地过程中真正遇到的坑、想明白的原理还有可以直接复用的代码逻辑一次性讲清楚。适合那些正在从单体往微服务迁移或者已经在微服务里被数据不一致问题折磨过的开发者看。1. 先聊几句这个项目到底在折腾什么先说背景。我手头的模拟项目X是一个典型的三服务结构订单服务负责创建订单库存服务负责扣减库存支付服务负责扣减余额。一开始三个服务共用一个数据库所有操作都在同一个事务里用 Spring 的Transactional就完事了简直不要太舒服。后来业务拆开三个服务各自连各自的库问题就来了订单创建成功了库存扣减失败了或者支付扣款成功了但订单状态没更新整个系统处于一个“客户不知道订单到底成没成”的尴尬状态。这时候项目里开始有人提分布式事务我之前第一反应是“能用本地事务解决的就别折腾”但现实逼迫我不得不面对这个坎。分布式事务之所以难是因为它要处理的不再是一个数据库引擎内部的行锁和日志而是多个数据库、多个服务之间的原子性和一致性。最典型的说法是 CAP 理论在分布式环境下一致性Consistency、可用性Availability、分区容错性Partition tolerance三者不可兼得。分布式事务本质上是“在分区容错的前提下尽量往一致性和可用性两侧去折中”。Seata 提供的四种模式就是四种“妥协方式”模式一致性级别隔离性业务侵入性性能影响开发成本AT最终一致较弱需人工策略低靠中间件中等全局锁开销低TCC最终一致由业务控制高需写三组逻辑中等高Saga最终一致弱中偏编排较低高XA强一致强低靠数据库高性能损失明显低Spring JDBC事务强一致强单库极低低极低先说一个大方向如果业务还在一个库里别碰分布式事务如果非跨库不可再考虑 Seata。后面我会一句一句拆。1.1 本地事务和全局事务的边界在哪里很多人上来就想上 Seata其实是没搞懂边界在哪。Spring JDBC 事务或者更准确地说Spring 对本地事务的管理管的是“同一个数据源里的多个数据库操作”。它依赖数据库本身的 ACID 特性比如 InnoDB 的 undo log 和 redo log保证事务回滚时能把数据恢复到之前的状态。而 Seata 这类全局事务管的是“多个独立数据源之间的操作协调”。它自己不管 SQL 执行而是通过拦截、代理、补偿等手段把多个本地事务串成一个逻辑上的全局事务。所以可以把 Seata 理解成一个“事务协调器”它自己不存业务数据只负责记录和协调各个服务节点的分支事务状态。这点想不清楚后面看代码会一头雾水。2. 四种全局事务模式的底层原理拆解2.1 AT模式自动化补偿的同步方案AT 模式的核心思路是“自动补偿”加“全局锁”。它在第一阶段就是把业务 SQL 直接执行掉但执行前会在解析 SQL 时生成一份“前后镜像”也就是这条数据在操作前是什么样、操作后是什么样然后把镜像数据记录到一张undo_log表里。如果一切顺利全局事务提交时各个分支事务正常提交然后异步清理undo_log就可以。如果某个分支失败协调器会告诉所有分支回滚这时候 AT 模式并不是让数据库自己回滚事务因为事务可能已经提交了而是拿着undo_log里记录的前镜像拼一条反向 SQL 把数据改回去。注意AT 模式的回滚是逻辑回滚不是物理回滚。如果你的业务 SQL 里用了now()、uuid()这类非确定性函数回滚时无法精确还原成旧值只能尽力还原。这个在金融场景里要特别小心。全局锁的作用是为了避免“脏写”。比如订单服务的全局事务正在改库存表别的全局事务也想改同一条库存记录就需要等前一个事务释放全局锁。这就有个副作用并发不高的场景挺顺一旦热点数据并发高全局锁会变成明显的瓶颈。2.2 TCC模式业务侵入式的三阶段补偿TCC 模式的全称是 Try-Confirm-Cancel三阶段拆解Try尝试执行业务完成资源的检查和预留。比如库存服务在 Try 阶段先把库存冻住冻结的这部分不能卖给别的人。Confirm确认执行业务真正扣减库存使用 Try 阶段预留的资源。Cancel取消执行业务释放 Try 阶段预留的资源。这块和 AT 最大的区别在于“业务代码得自己写补偿逻辑”。AT 模式里回滚是中间件自动生成的TCC 模式里回滚逻辑得你自己一个一个方法写。好处是隔离性由业务自己把控灵活度高坏处是开发量直接翻倍而且 Try/Confirm/Cancel 三个方法都要做成幂等这个非常考验设计能力。我用一个通俗的说法AT 是“你只管干活中间件帮你擦屁股”TCC 是“你自己把活干到一半可能干砸的预案都提前写好”。实际落地 TCC 时还有一个特别烦人的问题——空回滚。如果一个分支的 Try 还没执行成功全局事务就回滚了结果协调器把这个分支的 Cancel 也调过来了这时候你 Cancel 里不能直接执行“释放资源”的逻辑因为资源根本没预留过。这个场景必须单独判断业界通常用一个事务控制表记录执行状态在 Cancel 方法里判断状态再做处理。2.3 Saga模式编排式长事务的最终一致性Saga 模式和 TCC 类似也是靠补偿逻辑但它适合更长的业务流程。它的核心思想是把一个长事务拆成一组有序的子事务每个子事务都对应一个补偿事务。正常流程就按顺序执行如果某个子事务失败就反向依次调用前面所有已成功子事务的补偿事务。Seata 的 Saga 模式通常配合状态机引擎来用。你可以把每一步定义成状态节点节点和节点之间的流转用 JSON 来描述失败时自动走补偿路径。它比 TCC 更适合跨服务极多、流程极长的场景比如一个下单流程里除了订单、库存、支付还要调用积分服务、消息通知、物流分配。但 Saga 有几个先天问题它没有隔离性中间状态对外可见。比如库存扣减了但订单还没完成其他用户看到的就是库存少了。补偿逻辑同样需要你自己写而且补偿之后业务的数据状态可能不是完全恢复而是“业务上等价”的状态。流程一旦很长状态机配置会变得特别臃肿排查问题难度也会上升。我实际用下来的体会是Saga 适合那种“一个步骤失败了前面步骤可以业务性补偿”的场景比如订单取消退回库存、退回优惠券不适合那种“必须强一致”的场景比如余额扣减和流水记录。2.4 XA模式数据库层的分布式事务标准XA 模式是数据库层面支持的两阶段提交协议。它的流程是阶段一prepare各分支数据库执行 SQL但不提交只向事务协调器报告“我准备好了”。阶段二commit/rollback协调器收到所有分支的 prepare 结果若全部成功则通知所有分支提交任一个失败则通知所有分支回滚。XA 的强一致性是它最大的优点事务要么全部成功要么全部失败不存在中间状态。但它的问题也很明显锁持有时间太长。从第一阶段开始数据库资源就被锁住直到第二阶段结束所以高并发场景下性能损耗很大。对数据库的兼容性有要求比如 MySQL 在 XA 的某些场景下如果崩溃恢复处理不当会有数据不一致的风险。实现现在还是基于数据库驱动层Seata 的 XA 模式会帮你协调分支但本质上没有改变 XA 的性能瓶颈。我的建议很简单除非业务强一致性要求极高同时并发量可控否则别选 XA。很多时候强行把一个分布式场景变成强一致场景反而会把性能拖垮。3. 事务选型指南先回答五个问题再写代码3.1 五分钟方案筛选流程我每次做技术选型都会强迫自己过一遍这五个问题这个业务是否真的需要分布式事务单纯只在一个服务内部跨表操作那就别上分布式事务。业务能接受多长时间的最终一致如果可以接受秒级或分钟级的一致优先考虑 AT 或 Saga如果需要立刻强一致才考虑 XA。团队有没有能力维护大量补偿逻辑补偿逻辑多的话TCC 和 Saga 的成本会很高。核心链路的数据并发高不高高并发场景下 AT 和 XA 都容易出锁问题。业务数据能否通过幂等和重试来兜底如果可以用“幂等 定时对账”的最终一致性方案甚至可以不依赖分布式事务框架。这五个问题过一遍基本上选型就清楚了。3.2 不同场景的推荐方向我整理了一个简单的决策对照业务场景推荐方案理由单库多表操作Spring JDBC事务本地事务足够无需额外中间件跨库但整体事务时间短Seata AT侵入性低回滚自动适合大多数互联网场景涉及外部系统调用如短信、云服务TCC 或 Saga外部系统不支持全局事务必须走补偿逻辑长业务流程步骤多Saga状态机编排清晰各自补偿可复用金融强一致要求并发不高Seata XA强一致但需评估性能数据不一致可以靠对账修复不用分布式事务定时任务 幂等标识最省事注意我见过很多团队在“外部系统调用”场景硬上 AT 模式结果发现自己根本控制不了外部系统的数据回滚最后还得回到手工补偿。AT 只适合所有分支都是自己服务、自己数据库的情况。3.3 什么时候应该坚持用 Spring JDBC事务前面花了一堆篇幅说 Seata但我要泼一盆冷水Spring JDBC 事务仍然是效率最高的方案而且在很多场景下根本不该被替换。如果你只有一个数据库不管你拆成多少个微服务只要它们都连同一个数据库就仍然可以用同一个本地事务。因为事务的原子性由 InnoDB 保证跨服务调用只是在网络层加了点延迟不影响数据库层面的数据一致性。另外即使真的涉及多个数据库有些业务也完全可以通过“先改主库、异步操作从库”的方式规避分布式事务。比如生成订单时只写订单库库存扣减通过消息队列异步来做配合消息的重试和幂等消费照样能达到最终一致。这种方案虽然复杂但比 Seata 更轻量性能损耗也更小。所以选型时先想想是不是真的需要“全局事务”别因为看到“微服务”三个字就直接跳到 Seata。4. 上手实操Spring Boot 4 Java 25环境下的完整代码这一节我直接给你能跑的代码骨架。为了省篇幅只保留关键代码但逻辑是完整的。4.1 环境与依赖准备先说一下版本约束。我这边目前是用 Java 25 的编译目标Spring Boot 4 的依赖管理默认已经适配了 Jakarta EE 10。Seata 方面我用的 2.x 版本具体小版本你按当时的发布状态选依赖坐标大概是这样dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version2.x.x/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency如果是 Spring Boot 4记得检查 Seata 的 starter 是否适配了新的自动配置机制有些老版本在 Spring Boot 3 时就要手动排除部分自动配置类这个问题我放在避坑章节讲。4.2 Spring JDBC本地事务代码先用一个最经典的订单创建逻辑演示本地事务。假设订单表和库存表都在同一个库里Service public class OrderLocalService { Transactional(rollbackFor Exception.class) public OrderDTO createOrder(OrderCreateDTO dto) { // 1. 插入订单 OrderDO order new OrderDO(); order.setUserId(dto.getUserId()); order.setAmount(dto.getAmount()); order.setStatus(0); orderMapper.insert(order); // 2. 扣减库存 int rows stockMapper.deduct(dto.getSkuId(), dto.getQuantity()); if (rows 0) { throw new BizException(库存不足); } return OrderConverter.toDTO(order); } }Transactional的原理是 Spring 通过 AOP 生成代理在方法开始前开启数据库事务方法执行完毕后提交如果方法抛出异常且满足rollbackFor条件就回滚所有已执行的 SQL。这个方法表面看着简单但其实数据库层面已经帮你把原子性、隔离性、持久性都处理了这也是本地事务一直“好用”的核心原因。如果业务场景需要更细粒度地控制事务边界比如方法内部有部分操作不必参与事务那可以改用TransactionTemplate的方式Service public class OrderServiceWithTemplate { private final TransactionTemplate transactionTemplate; public OrderServiceWithTemplate(PlatformTransactionManager txManager) { this.transactionTemplate new TransactionTemplate(txManager); // 配置隔离级别等 transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); } public void doInTx() { transactionTemplate.executeWithoutResult(status - { orderMapper.insert(...); accountMapper.deduct(...); }); } }4.3 Seata AT模式接入AT 模式是我用得最多的模式代码侵入性最低。首先要在每个参与全局事务的数据库里创建undo_log表CREATE TABLE IF NOT EXISTS undo_log ( id BIGINT NOT NULL AUTO_INCREMENT, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME NOT NULL, log_modified DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4;然后配置 Seata 客户端我习惯用 Nacos 做注册中心和配置中心seata: enabled: true application-id: order-service tx-service-group: my_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP username: nacos password: nacos config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP service: vgroup-mapping: my_tx_group: default业务代码只需要在全局事务入口加一个注解Service public class OrderBizService { GlobalTransactional(rollbackFor Exception.class) public OrderDTO createOrder(OrderCreateDTO dto) { // 调用订单服务内部是本地事务 orderClient.create(dto); // 调用库存服务另一个服务另一个库 stockClient.deduct(dto.getSkuId(), dto.getQuantity()); // 调用支付服务 paymentClient.pay(dto.getOrderNo(), dto.getAmount()); return orderClient.query(dto.getOrderNo()); } }注意这里的orderClient、stockClient、paymentClient是模拟的项目X内部接口真实项目里会是 Feign 或 WebClient 调用。Seata 在执行过程中会自动把全局事务的 XID 通过调用链传递到下游服务下游服务里对应的数据源代理会检测到 XID 并注册成本地分支事务。这里有个细节AT 模式的实现依赖数据源代理。用 Seata 之后业务里不能直接用原始的DataSource而是要用DataSourceProxy包一层或者直接用 starter 自动创建好的代理数据源。Spring Boot 集成 Seata 时这一步通常是自动完成的但如果你自己配置了多个数据源要小心没被代理的那个数据源不会参与全局事务。4.4 TCC模式落地TCC 模式下你要自己定义接口加上 Seata 的注解。我用库存服务的 Try 阶段来举例LocalTCC public interface InventoryTccService { TwoPhaseBusinessAction( name inventoryTccAction, commitMethod confirm, rollbackMethod cancel ) boolean prepare( BusinessActionContext context, BusinessActionContextParameter(paramName skuId) String skuId, BusinessActionContextParameter(paramName quantity) Integer quantity); boolean confirm(BusinessActionContext context); boolean cancel(BusinessActionContext context); }实现类里Try 阶段做“冻结库存”Confirm 阶段做“扣减冻结库存”Cancel 阶段做“释放冻结库存”Service public class InventoryTccServiceImpl implements InventoryTccService { Autowired private StockFreezeMapper freezeMapper; Override public boolean prepare(BusinessActionContext context, String skuId, Integer quantity) { // 1. 校验库存 int rest stockMapper.getAvailableStock(skuId); if (rest quantity) { throw new BizException(库存不足); } // 2. 冻结库存 int rows stockMapper.freeze(skuId, quantity); // 3. 插入冻结记录用于幂等和空回滚判断 freezeMapper.insert(new StockFreezeDO(skuId, quantity, TRY)); return rows 0; } Override public boolean confirm(BusinessActionContext context) { String skuId (String) context.getActionContext(skuId); Integer quantity (Integer) context.getActionContext(quantity); // 1. 将库存从“冻结”变为“已扣减” stockMapper.confirmDeduct(skuId, quantity); // 2. 更新冻结记录状态 freezeMapper.updateStatus(skuId, CONFIRM); return true; } Override public boolean cancel(BusinessActionContext context) { String skuId (String) context.getActionContext(skuId); Integer quantity (Integer) context.getActionContext(quantity); StockFreezeDO freezeRecord freezeMapper.findBySkuId(skuId); // 空回滚判断如果 Try 阶段都没成功Cancel 直接返回 true if (freezeRecord null) { return true; } // 幂等判断已经 CANCEL 过的直接返回 if (CANCEL.equals(freezeRecord.getStatus())) { return true; } // 释放冻结库存 stockMapper.releaseFreeze(skuId, quantity); freezeMapper.updateStatus(skuId, CANCEL); return true; } }这段代码看着还好但真去写的时候你得确保 Prepare、Confirm、Cancel 三个函数全部是幂等的否则网络超时重试会把你坑死。我后面避坑部分会细讲。4.5 Saga状态机代码Saga 模式在 Seata 里通常配合状态机引擎。你把流程定义成一个 JSON然后调用状态机服务。我简化为一个浓缩的示例{ name: orderSaga, startState: createOrder, states: { createOrder: { type: ServiceTask, serviceName: orderService, serviceMethod: createOrder, next: deductInventory, catches: { exception: { next: fail } } }, deductInventory: { type: ServiceTask, serviceName: inventoryService, serviceMethod: deductInventory, next: createPayment, catches: { exception: { compensateState: compensateInventory, next: fail } } }, compensateInventory: { type: ServiceTask, serviceName: inventoryService, serviceMethod: releaseInventory }, createPayment: { type: ServiceTask, serviceName: paymentService, serviceMethod: createPayment, next: success }, fail: { type: Fail }, success: { type: Success } } }调用端代码是这样Service public class SagaOrderService { Autowired private StateMachineEngine stateMachineEngine; public void createOrder(OrderCreateDTO dto) { String stateMachineName orderSaga; MapString, Object businessParams new HashMap(); businessParams.put(dto, dto); stateMachineEngine.start(stateMachineName, null, businessParams); } }Saga 状态机的好处是流程可视化每一步的调用参数和返回结果都录下来了出问题好排查。坏处也明显如果你没在里面处理好补偿流程的幂等和超时状态机会出现“补偿过程中再次失败”的窘境需要额外的手工运维介入。4.6 XA模式落地XA 在 Seata 里的使用相对简单主要工作在于配置数据源为 XA 数据源并在全局事务入口加GlobalTransactional。假设用 HikariCP 来管理连接实际上需要换成支持 XA 的XADataSourceConfiguration public class XaDataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource.xa) public DataSource xaDataSource() { return new org.postgresql.xa.PGXADataSource(); } Bean public DataSource dataSource() { // 用 Seata 的 DataSourceProxy 做一层包装让 Seata 能感知到 XA 事务 return new DataSourceProxy(xaDataSource()); } }然后和 AT 模式一样Service public class XaOrderBizService { GlobalTransactional(rollbackFor Exception.class) public void createOrder(OrderCreateDTO dto) { orderClient.create(dto); stockClient.deduct(dto.getSkuId(), dto.getQuantity()); paymentClient.pay(dto.getOrderNo(), dto.getAmount()); } }XA 的性能问题在前面已经说过了这里不再重复。只提醒一句如果你的项目用的数据库连接池不支持 XA那这段代码跑起来多半会报“connection is busy with another transaction”之类的错误所以在测试环境就要把数据源这一类问题暴露出来。5. 落地避坑那些文档里不写但真实存在的坑5.1 版本兼容与自动配置Seata 对 Spring Boot 版本的适配从来都是一个大坑。Spring Boot 4 的自动配置机制和老版本有差异特别是ConditionalOnClass、自动装配顺序、spring.factories和META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这几个配置文件稍不留神就会导致 Seata 的代理数据源没生效。我踩过最典型的坑是GlobalTransactional方法里调用的 SQL 没走 Seata 的代理数据源导致全局事务里的分支根本没有被注册。解决方案是检查启动日志里有没有“Seata Auto Configuration”那一段以及跟踪 XID 是否在下游服务里出现。5.2 AT模式的脏读和回滚日志AT 模式用的是“全局锁 镜像回滚”因此读未提交数据是有可能发生的。如果业务对中间状态很敏感比如报表系统、对账系统你在查这些数据的时候需要加上SELECT ... FOR UPDATE或者把查询请求路由到另一个数据源。这个不是 Seata 帮你处理的你得自己在业务层设计。还有一个很隐蔽的坑undo_log表里的rollback_info是二进制大对象如果业务 SQL 涉及很多行的大批量更新这个字段可能变得非常大导致 AT 模式的性能骤降。千万别在undo_log上做分区、做归档老老实实按官方建议用最小的表结构。5.3 TCC模式的幂等、空回滚与防悬挂TCC 的坑基本集中在三个词上幂等、空回滚、防悬挂。幂等网络抖动会导致 Confirm / Cancel 被重复调用你要保证二次调用不会再次扣减或释放库存。空回滚Try 没执行就执行了 CancelCancel 里不能直接操作资源。防悬挂如果 Cancel 先执行完了后面 Try 的调用才到达那 Try 就不能再执行了否则资源会被冻结而没人释放。这三个问题必须用一张事务控制表来解决每一笔业务操作在表里都有一个状态记录。我通常在prepare里插入或更新状态confirm/cancel里先检查状态再执行动作。5.4 Saga状态机的超时与补偿失败状态机里面的每个 ServiceTask其实就是一个 RPC 调用。RPC 超时是家常便饭因此状态机引擎的“重试”和“补偿”很容易互相打架。比如某个步骤 RPC 超时了状态机以为执行失败去调补偿逻辑但实际原服务已经执行成功了然后补偿逻辑把不该释放的资源释放了。解决思路是在每个服务方法里都做“幂等标识判断”这个标识最好就是订单号或全局事务 XID 这类业务上唯一的编号。只有拿到幂等标识确认这条记录当前的状态才决定是否继续执行还是直接返回。5.5 XA连接池与性能瓶颈XA 的坑主要是连接管理。因为 XA 事务里一个连接要跨越两个阶段数据库连接会被长时间占用。连接池设置太小很容易把连接池打满导致整个系统瘫痪。还有一点MySQL 的 XA 事务如果碰上连接中途断开那个事务可能变成悬空事务需要 DBA 手工清理XA RECOVER列表。这个排查成本非常高所以在选型 XA 之前一定要让运维确认团队有没有能力处理这类数据库层面的异常。5.6 本地事务与全局事务的嵌套当你把Transactional和GlobalTransactional混在同一个服务里时务必要注意传播行为。如果全局事务方法内部调用了另一个加了Transactional的本地方法默认的传播行为会把本地事务合并到已有事务里这在 Spring 中是正常的。但当这个本地方法变成了独立服务它本身就是一个本地事务分支如果本地事务自己先提交了那全局事务回滚时只能靠 AT 的镜像回滚而不是数据库事务回滚。因此我建议被全局事务调用的服务方法内部都不要再手动去控制事务边界让 Seata 去管。否则很容易触发“分支事务提交后无法正常回滚”的问题。5.7 多数据源与事务管理器冲突项目一旦引入多个数据源Spring 里默认的事务管理器就不能再只配一个了。你需要显式指定事务管理器Bean Primary public PlatformTransactionManager txManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }但如果把 Seata 的代理数据源也配进来事情会变得更复杂。我的建议是业务服务内部的本地事务还是走 Spring 管理跨服务的全局事务才走 Seata不要混成一锅粥。边界清晰排查问题也更容易。6. 我自己的经验看法把四种模式都刷一遍之后我的个人体会是分布式事务本质上没有银弹每种方案都是拿某方面的性能、开发成本或一致性去换别的东西。在我现在做的这类互联网业务里如果让我重新选我会优先用 AT 模式解决 80% 的跨服务事务需求TCC 只用在那些真正需要业务防悬挂和精细资源控制的支付类场景Saga 则留给长流程多服务的业务流程。XA 除非是监管要求强一致否则我真不愿意碰。最后再分享一个小技巧不管选哪种模式都要给业务表加“幂等键”和“操作状态”字段。分布式事务的很多问题最后都可以靠“重试 幂等”来兜底。事务框架能帮你解决一致性的逻辑问题但网络超时和重复调用永远需要你自己在设计表结构时就提前做好准备。