恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Seata分布式事务全解析:从原理到订单库存落地
首页
资讯中心
/
Seata分布式事务全解析:从原理到订单库存落地
Seata分布式事务全解析:从原理到订单库存落地
发布时间:2026/10/10 6:10:15
搞过订单系统和库存系统对接的同学几乎都遇到过同一个噩梦订单创建成功了库存却扣减失败用户拿着订单号来找你后台查出来库存还纹丝不动超卖订单一串串冒出来。这种跨服务、跨库的写操作传统本地事务根本管不住因为事务边界已经从单库跨越到了多个独立服务之间。今天想好好聊聊分布式事务这个话题重点把 Seata 这套方案讲清楚——它是什么、怎么运作以及在一个真实的“订单库存”场景里从零落地新手也能照着做。我会把原理部分也尽量讲得接地气不会上来就丢一堆概念。前半部分给基础薄弱的同学补背景中间给架构设计和模式对比后半段是完整实操代码和不常见但很关键的问题排查。整个过程走下来你至少能独立判断自己的系统适不适合上 Seata、应该选哪种模式、部署完怎么测、踩坑了怎么查。1. 分布式事务的前世今生为什么订单扣库存会“翻车”1.1 从一个线上订单事故聊起有一次我们线上做了一次会员抢购活动流量还没到峰值客服那边就开始出现“下单成功但库存显示已扣”的投诉。排查链路拉出来订单服务写入 order 表成功然后调用库存服务扣减 stock 表结果库存服务这边因为一个慢 SQL 超时异常被上游捕获订单回滚了。看着像是“回滚成功”但库存库里那条扣减记录其实已经提交了。这就是典型的分布式环境下两个服务操作两个独立数据库导致的一致性问题。单体应用时代订单和库存放在同一个库表里一条UPDATE和一个INSERT包在同一个事务里要么全成要么全败。拆成微服务之后数据库被物理拆开Transactional只能管住本地那一个库跨服务的调用链路根本不认你这一套。用户在界面上看到的是“下单失败”后台却留着脏数据这种账不平的情况每一个真正跑过订单系统的人都懂有多头疼。1.2 分布式环境下的三个核心难题第一个难题是原子性。订单创建、扣库存、扣余额、加积分这些动作分散在多个服务里任何一个步骤失败其他步骤都得跟着回滚但一个全局的“回滚开关”并不存在全靠自己协调。第二个难题是网络不确定性。服务之间的调用走网络什么时候超时、什么时候丢包、什么时候对端处理了一半进程突然挂了你根本没法精确判断。很多时候你以为对方失败了实际上它已经提交成功这就让状态判定变得非常棘手。第三个难题是隔离性。两个并发请求同时抢扣同一个商品的库存A 事务读了库存数量是 10B 事务也读到 10两边都扣成 9最后库存真实的扣减只发生了一次甚至发生两次这就产生了超卖。分布式事务框架除了要解决“全部成功或全部回滚”还要处理并发期间的数据可见性和锁冲突问题这比单纯的原子性复杂得多。要理解 Seata 的设计逻辑必须先接受一个前提在分布式环境里想做到完美的强一致代价极高性能往往不可接受。所以现实中大多数团队选择的是“最终一致性”路线——允许中间状态存在但保证业务最终是正确的。Seata 的几种模式里AT、TCC、Saga 本质上都是在不同场景下追求“尽快收敛到一致状态”的手段而不是像单机事务那样的瞬时强一致。1.3 主流方案盘点消息队列、XA、TCC、Saga 和 Seata每个分布式事务问题都有好几条路可以走这里先说透各个方案的定位后面你才知道 Seata 到底解决的是哪一层问题。第一条路是消息队列做最终一致。订单创建成功之后发一条 MQ 消息给库存服务去扣减库存服务消费消息异步执行。这个方案的好处是吞吐量高、业务无侵入但坏处是如果消息发出去了库存服务一直消费失败订单又已经对用户可见期间就存在“超卖窗口”。所以通常需要配合定时对账和人工补偿。这里适合非核心链路比如加积分、发短信。第二条路是数据库层面的 XA 协议。各数据库厂商原生支持两阶段提交通过事务管理器统一协调资源能拿到很强的数据一致性。但 XA 的坑也很明显事务期间所有参与者都要锁住资源数据库性能会断崖式下降业务高峰期根本扛不住。另外很多中间件和连接池对 XA 支持不完善用起来限制多。第三条路是 TCC。它把事务拆成 Try、Confirm、Cancel 三个阶段业务方自己实现三套逻辑控制力和灵活性最强但开发量非常大还得自己处理空回滚、防悬挂、幂等等细节对团队要求高很容易写出隐性 bug。第四条路是 Saga。专门服务长流程事务把一个全局事务拆成多个本地事务每个本地事务有对应的补偿操作一路正跑挂了反向补偿。它不锁资源适合跨多个服务且执行时间很长的流程比如旅游预订、贷款审批但它没有隔离性需要业务层面自己控制并发。Seata 出现的意义在于它把这些模式的“公共底座”抽出来统一了事务协调的框架层逻辑然后分别提供了 AT、TCC、Saga、XA 四种模式的接入方式。尤其是 AT 模式它对业务代码几乎无侵入只靠拦截 SQL 和反向补偿日志大大降低了分布式事务的上手门槛。很多团队就是从 Seata 的 AT 模式开始才敢把本地事务直接升级为分布式事务。2. Seata 核心架构与原理拆解2.1 三个核心角色TC、TM、RM 怎么配合Seata 的架构一句话就能说清一个事务协调中心外加一堆分支事务参与者的配合。但为了让新手不看懵我拆成三个角色逐个讲。TC 是 Transaction Coordinator事务协调器也就是我们常说的 Seata Server。它单独部署负责接收全局事务的注册、记录事务状态、通知各个参与者提交或回滚。你可以把它理解为“全局事务大脑”所有事务状态都汇聚到它这里。TM 是 Transaction Manager事务管理器。它通常集成在发起全局事务的那个服务里比如订单服务。业务方法通过GlobalTransactional注解标记后TM 会向 TC 申请开启一个全局事务拿到全局事务 IDXID在业务结束时向 TC 发出全局提交或全局回滚的请求。RM 是 Resource Manager资源管理器。它负责管理每个分支事务下的本地资源比如订单数据库、库存数据库。RM 在执行完本地 SQL 后向 TC 注册分支事务报告执行状态当 TC 下发提交或回滚指令时RM 负责真正执行二阶段的本地提交或数据回滚。整个事务生命周期的流程大致是这样订单服务带着GlobalTransactional的方法开始执行TM 向 TC 注册全局事务并拿到 XID。接下来无论订单服务调用库存服务还是积分服务XID 都会通过调用链传播出去下游服务的 RM 在执行本地事务时会拿着这个 XID 向 TC 注册分支事务。当主业务方法执行完TM 根据执行结果决定向 TC 发起全局提交或回滚。TC 收到指令后依次通知所有分支事务的 RM 执行二阶段操作。RM 收到提交就清理事务日志收到回滚就生成反向 SQL 把数据恢复原样。这里有一个容易被忽略的重点XID 的传播是整个框架正常工作的基础。后面实操会专门讲 Feign 和 RestTemplate 场景下 XID 怎么传递以及新的异步线程里为什么会丢 XID。2.2 AT 模式不用大改业务代码的糖衣炮弹AT 模式是 Seata 最常见、也最被低估的模式。它厉害在什么地方业务开发几乎不需要写额外的补偿逻辑只需要在方法上加一个注解Seata 会自动帮你完成二阶段的提交或回滚。原理细节拆开看AT 模式的核心是“拦截 SQL 记录变更前后镜像 反向补偿”。一阶段RM 拦截业务 SQL解析出 SQL 的影响范围执行 SQL 前生成“前镜像”执行 SQL 后再生成“后镜像”把这两份数据连同 XID、分支事务 ID 一起写入本地数据库的undo_log表。然后这段本地事务直接提交。二阶段如果 TC 下发的是提交指令RM 只需异步删除对应的undo_log记录如果下发的是回滚指令RM 根据undo_log里保存的镜像数据生成反向 SQL 把数据恢复成执行前的样子。这套设计里有一个全局锁机制也是 AT 模式最需要关注的东西。Seata 在 TC 端维护了一张锁表记录哪些数据行正被哪些全局事务持有。一个分支事务要修改某行数据前必须先去获取这行数据的全局写锁如果另一事务正持有锁当前事务只能等待或者超时报错。这样一来两个全局事务不会同时对同一行数据进行覆盖式修改就避免了幻读和脏写。AT 模式的好处有二第一业务代码侵入极低原有UPDATE、INSERT和DELETE不用改只要把数据源代理换成 Seata 的数据源代理即可。第二有undo_log表作为依赖不需要业务方自己做补偿。缺点也很明显全局锁的引入会让高并发热点行的写操作变成串行性能瓶颈明显且要求业务表必须使用支持事务的存储引擎比如 InnoDB否则 undo_log 根本没法和业务表保持同步。我在后面的踩坑篇里会专门展开这些限制。2.3 TCC 模式Try-Confirm-Cancel 与业务入侵权衡当数据表不希望被 Seata 锁表或者要求更强的隔离性时就该选择 TCC 模式了。TCC 把事务拆成三步Try 阶段检查业务资源并预留资源Confirm 阶段真正提交业务操作Cancel 阶段把预留的资源释放出来。比如下单时Try 阶段先锁定库存数量Confirm 阶段实际扣减库存Cancel 阶段则把已锁定但未扣减的库存释放回去。TCC 的适用范围很广尤其适合跨行转账、账户余额操作这种资源性强的业务。但它比 AT 模式要累得多业务方必须自己实现 Try、Confirm、Cancel 三套逻辑而且这里有三件事必须自己做扎实。第一件是幂等控制。Confirm 和 Cancel 可能被 TC 多次调用你的接口必须保证同一分支事务第二次执行不会重复扣款或重复加余额。第二件是空回滚。因为 Try 阶段可能因为网络超时压根没执行但 TC 已经发出 Cancel这时 Cancel 的调用必须能识别出 Try 没执行过防止反向补偿一个不存在的资源。第三件是防悬挂。Cancel 如果先到了后续迟到的 Try 就不应该再执行否则会产生一个无人清理的锁定资源。从我的经验看TCC 是业务团队普遍容易低估开发量的模式。一个 TCC 服务代码量往往能达到 AT 模式的三到五倍而且一旦链路拉长排查每个分支到底处于什么状态也会非常费劲。所以只有在确实无法接受 AT 模式的全局锁、或者业务上已经有大粒度资源预留需求时才推荐优先考虑 TCC。2.4 Saga 和 XA 模式特定场景下的备选项Saga 模式适合长流程事务。和一阶段直接锁定资源不同Saga 把一个流程拆成若干小步骤每步一个本地事务并配有相应的补偿事务。任何一个步骤失败就按照相反方向逐一批量执行补偿。它的“补偿”不是回滚数据库而是执行一套对外的冲正逻辑比如取消订单调退款接口。由于它不持有全局锁Saga 的吞吐量是最高的但代价是没有隔离性你需要自行处理并发覆盖的问题。比如两个 Saga 同时修改同一账户余额可能会出现金额互相覆盖。适合的场景是超级长链路、耗时长且不要求强隔离的流程。XA 模式就简单直白多了它把 Seata 作为 XA 事务协调器把分支事务交给数据库原生 XA 协议去处理。这种方式能获得数据库层面的强一致保证实现也相对简单但它继承了 XA 的所有老毛病事务期间资源锁持有时间长数据库性能下降明显并且要求底层数据库和驱动都能稳定支持 XA。我一般只在非核心的低频强一致需求场景里才建议用 XA。四种模式做个对比帮你选型模式一致性级别业务侵入度锁资源适用场景主要代价AT最终一致极低全局锁常规订单、库存、账户强一致业务热点行并发下降TCC最终一致高业务预留锁跨行转账、资金类、强隔离需求开发量大需自己处理防悬挂等Saga最终一致中不锁长流程、高吞吐业务无隔离需自行防覆盖XA强一致低数据库锁低频、强一致特定场景性能差依赖数据库支持我个人的建议是第一次接触 Seata从 AT 模式入手是性价比最高的路径。它不需要先理解沉睡的 Saga 补偿链路也不需要写三套 TCC 接口只要把环境搭起来、写一个 demo、跑一次正常命中和回滚你就能直观感受到分布式事务框架到底在做什么。3. Seata 从零部署与 Spring Boot 集成实操3.1 部署 Seata Server先把 TC 跑起来不管后面接什么模式第一步都是把 TC 也就是 Seata Server 部署起来。这里我以 Seata 1.6.x 版本为例现在 2.x 也很多人用但 1.6.x 的资料和稳定性都比较成熟新手按这个版本走不容易掉坑。先去官方仓库的 Release 页面下载seata-server的二进制包解压后的目录结构大概是bin、conf、lib和logs。1.5 版本之后的配置文件全面迁移到了conf/application.yml老版本常见的registry.conf和file.conf在 1.5 以后已经逐步被替代如果你下载的版本比较新直接看application.yml即可。在application.yml中核心配置有两块registry 和 config。理解这两块的定位很关键registry 解决“客户端怎么找到 TC 的地址”config 解决“TC 自己的配置放在哪儿”。新手本地直接跑通最省事的方式是用file模式server: port: 7091 spring: application: name: seata-server seata: registry: type: file config: type: file store: mode: file然后进入bin目录启动服务sh seata-server.shWindows 环境用seata-server.bat。启动成功后在日志中能看到类似Server started的输出同时默认监听 8091 端口。敲一下telnet 127.0.0.1 8091确认端口可用。注意了7091 是控制台 HTTP 端口8091 才是 RPC 通信端口别搞混。生产环境一般不会用 file 模式因为 TC 重启后事务状态会丢而且多实例部署没法共享状态。至少要把store.mode改成db让全局事务会话持久化到数据库里seata: store: mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://your-mysql-host:3306/seata?useUnicodetruecharacterEncodingutf8 user: your_user password: your_password数据库里需要初始化几张表global_table、branch_table、lock_table。这些建表 SQL 在 Seata 源码或发行包的script/server/db/mysql.sql里都有直接执行即可。日常排障时我也会时不时去这几张表里看全局事务当前的状态后面排查篇会讲怎么看。3.2 给订单和库存服务装 Seata ClientTC 跑起来之后接下来就是让订单服务和库存服务接入 Seata Client。这一步最核心的点是版本对齐Spring Boot 2.x 时代最稳的组合是 Seata 1.6.x 对应seata-spring-boot-starter1.6.x别把 client 和 server 版本偏离太远否则可能出现协议兼容问题。先加依赖dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.6.1/version /dependency然后在application.yml里配置 Seata Client。这里最关键的有三件事事务分组、注册中心和配置中心。事务分组是一个逻辑上的概念用来把一组参与同一事务的服务归纳到一起比如订单服务、库存服务都配置成my_test_tx_groupSeata 会根据分组名找到对应的 TC 集群地址。seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: file config: type: file注意事务分组有个翻译过程。Client 拿着tx-service-group去找 TC 地址时会到配置中心或注册中心里找一个 key比如vgroupMapping.my_test_tx_group它的值通常是default这样的 TC 集群名然后再根据集群名找实际节点。这个映射关系忽略的话日志里经常会看到no available service found或can not get cluster name的报错新手极容易在这里卡壳。接下来是数据源代理这是 AT 模式的另一个关键点。不使用 Seata 代理数据源框架就无法拦截 SQL 生成 undo_log。直接新建一个配置类Configuration public class DataSourceProxyConfig { Bean ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return new DruidDataSource(); } Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSourceProxy); factory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/*.xml)); return factory.getObject(); } }这个配置类重点看两点。第一DataSourceProxy必须被SqlSessionFactory使用否则 MyBatis 连接的是原始数据源SQL 拦截等于没生效。第二如果项目里用了 MyBatis Plus操作方式类似但要注意把MybatisSqlSessionFactoryBean也换成 Seata 代理过的数据源。这个顺序一旦搞错最直接的后果就是事务日志表一直没数据回滚也完全不生效。最后是建表。AT 模式下每个库都必须建一张undo_log表CREATE TABLE IF NOT EXISTS undo_log ( branch_id BIGINT NOT NULL COMMENT branch transaction id, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGTEXT NOT NULL, log_status INT NOT NULL, log_created DATETIME NOT NULL, log_modified DATETIME NOT NULL, ext VARCHAR(100) DEFAULT NULL, PRIMARY KEY (branch_id), KEY idx_xid (xid) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;这张表放在业务数据库里和业务数据保持同库事务这是 AT 模式能够精准回滚的物理根基。包括 MySQL 的undo_log表在business db中字段名和版本对应关系官方 SQL 里都给全了直接复制执行即可。3.3 订单与库存落地的核心代码演示依赖、配置、表结构都准备好之后代码接入就很直接了。用一个最简单也最具代表性的场景用户下单之后创建订单并扣减库存。订单服务和库存服务是两个独立服务分别连接各自的数据库。订单服务这边入口方法上加GlobalTransactionalService public class OrderService { Resource private OrderMapper orderMapper; Resource private StockFeignClient stockFeignClient; GlobalTransactional(name create-order-tx, rollbackFor Exception.class) public void createOrder(OrderCreateDTO dto) { // 1. 创建订单 Order order new Order(); order.setUserId(dto.getUserId()); order.setProductId(dto.getProductId()); order.setAmount(dto.getAmount()); order.setStatus(0); orderMapper.insert(order); // 2. 远程调用库存服务扣减库存 StockDeductDTO stockDTO new StockDeductDTO(); stockDTO.setProductId(dto.getProductId()); stockDTO.setCount(dto.getCount()); stockFeignClient.deduct(stockDTO); } }库存服务那边正常写扣减库存的 Mapper 操作不需要任何分布式事务注解Service public class StockService { public void deduct(StockDeductDTO dto) { Stock stock stockMapper.selectByProductId(dto.getProductId()); if (stock.getCount() dto.getCount()) { throw new BusinessException(库存不足); } stockMapper.deduct(dto.getProductId(), dto.getCount()); } }当订单服务的createOrder方法执行时TM 向 TC 注册全局事务拿到 XID。订单服务调用stockFeignClient时Seata 通过 Feign 拦截器自动把当前 XID 放进请求头库存服务那边再自动从请求头解析出 XID并注册为分支事务。这是 Seata 官方对 Feign 和 RestTemplate 的一等公民支持我实测下来只要依赖标准配置XID 都会自动传到下游。如果你的服务之间不是用 Feign 而是自定义 HTTP 调用那就需要手动把 XID 放进请求头同时处理入站时解析 XID。可以用RootContext.getXID()获取当前 XID把它放进 Header。接收方则要调用RootContext.bind()手动绑定 XID。这部分稍有点绕建议最好统一用 Feign 或 RestTemplate。调用链路正常执行完毕TM 向 TC 提交全局事务TC 通知库存服务这个分支事务“可以提交”库存服务清理掉undo_log里的记录。如果库存服务抛出异常TM 会向 TC 发起全局回滚TC 通知所有分支事务执行回滚订单服务和库存服务都会根据各自的undo_log恢复到执行前状态两边数据干干净净。这里有一个容易被误解的点GlobalTransactional只加在入口方法上下游服务不需要加。参与全局事务的判定依据是 XID 是否存在而不是是否有这个注解。你如果在下游服务的方法上也加了GlobalTransactional反而可能出现全局事务嵌套造成分支事务重复注册和报表混乱。3.4 验证与实测正常流程和回滚流程环境搭完代码写完之后一定要做一轮完整的实测别直接上生产。第一轮测正常流程调用下单接口看订单表多了一条记录库存表对应商品库存减少然后去查询两个库的undo_log正常情况下log_status为 1判断事务日志已被清理。如果undo_log里残留了大量log_status为 0 的记录说明二阶段清理异常大概率是 TC 没收到提交或者分支事务未注册成功。第二轮测回滚流程给库存服务人为制造一个异常比如把商品库存设为 0再调用下单接口。这时订单创建代码已经执行但库存服务会抛出“库存不足”异常。你去看订单表那条订单应该不存在因为它已经被反向 SQL 回滚了。同时undo_log表里会出现一条曾经的回滚记录log_status也是 1。整个过程最理想的状态就是对业务代码而言仿佛“什么都没发生过”两边数据都回到初始状态。第三轮测并发和重复调用用压测工具或脚本同时发起多笔相同商品的下单请求观察是否会出现“库存被扣多了”或“订单多了但库存没动”的情况。这一轮能很直观地暴露出全局锁冲突和 XID 传播问题。如果出现GlobalLockConflict类型的异常说明热点行并发超出了 AT 模式默认的锁等待时间后面就要从锁超时时间和热点拆分两个方向去调优。4. 常见问题排查与避坑实录4.1 问题速查表这里把我实战中遇到的高频问题整理成一张速查表方便你碰到问题时直接对照。现象可能原因解决方案项目启动报no available service found事务分组映射不对Client 找不到 TC 地址检查vgroupMapping分组配置确认 TC 集群名正确日志中大量branch transaction register failedTC 端口不通或注册中心配置不一致telnet 8091 端口检查 registry 配置触发异常后数据没有回滚数据源未被 Seata 代理或业务表不是 InnoDB检查DataSourceProxy是否被 MyBatis 使用确认建表引擎undo_log表中一直没有数据SQL 拦截没生效检查是否使用了代理数据源确认 AT 模式开启Feign 下游新增了全局事务嵌套下游服务也加了GlobalTransactional去掉下游注解XID 会自动传播新线程里操作数据导致脏数据异步线程中没有 XID新线程内手动绑定 XID或用RootContext.bind()传递高并发下热点商品下单报全局锁冲突同一行数据被多个全局事务争抢延长锁等待时间、拆分热点商品、减少事务范围TC 重启后全局事务状态丢失store.mode使用 file切换为 db 模式持久化事务状态回滚时报rollback info not foundundo_log被手工清理或镜像数据被覆盖检查清理任务确认事务期间未手工修改数据这些问题的排列是我按频率从高到低整理的。前三个问题占了新手上路阶段八成以上的报错基本都指向同一个根源Seata 的注册发现和事务分组链路没打通。我特别想说一下这个因为很多同学一开始会死磕代码其实只是配置文件里漏了一个 key。4.2 数据源代理和事务注解的坑数据源代理这块新手最容易犯的错就是把DataSourceProxy注册成 Bean 后就以为完事了但 MyBatis 的SqlSessionFactory里还引用着原始DataSource。之前我就见过一个同事Bean加了、包引入了、undo_log表也建了回滚就是不生效查了半天才发现sqlSessionFactory的setDataSource传的还是原始数据源。这种错误在本地很难发现因为正常流程看起来都是正常的只有废了好大劲手动把库存扣了再去触发回滚才暴露出来。事务注解的叠用同样值得注意。如果方法里既有Transactional又有GlobalTransactional务必要考虑事务传播行为。GlobalTransactional本身基于未被代理的数据源、以及和本地Transactional之间存在事务嵌套时容易产生连接被提前释放的问题从而在回滚时出现Connection is closed异常。我的建议是入口方法优先只放GlobalTransactional内部数据库操作的本地事务交由 Seata 拦截自动处理不要手动再加一层Transactional。还有一个坑是 Feign 的异步返回或自定义拦截器。如果你在 Feign 拦截器里干了重写请求头的事要小心别把 Seata 的 XID 头覆盖掉。公司里如果存在网关转发网关设置的头清理规则也可能把 XID 头给滤掉。最直接的验证方式是在库存服务的入口处打一条日志看看RootContext.getXID()是否能拿到值。4.3 性能瓶颈和调优方向Seata AT 模式最大的性能瓶颈不在 undo_log 的写入而在于全局锁。热点商品在促销时所有扣减库存事务都会扎堆到同一行记录上形成排队。缓解方向有三个。第一个方向是改全局锁等待超时时间。Client 端有参数可以调锁重试次数和锁等待时间但这只是缓兵之计治标不治本。真正把 TPS 拉起来还是要减少事务持锁时间也就是缩短分支事务的执行时长把慢 SQL 优化掉。我见过一个系统因为库存表上一个非索引字段的查询拖了几秒导致全场锁等待飚到上限所有下单全部失败。第二个方向是热点拆分。对库存这个场景可以把单一商品拆成多个子库存比如 100 件库存拆成 10 个库存桶每次随机选桶扣减。这个方案能显著降低同一行的锁竞争但实现复杂度高并且在回滚时也要精确处理返桶逻辑一般只在促销架构里使用。第三个方向是弱化大事务范围。检查你的GlobalTransactional方法里是否包含大量非必要远程调用比如下单时顺带调用短信服务、推荐服务。这些调用不参与核心一致性的话就要把它们移出全局事务让 Seata 只管订单和库存这两个强一致实体事务范围和持锁时间都会大幅缩短。同时 Seata Client 自身也有一些开关可以调整比如在资源允许填内存时调大提交缓冲把二阶段提交的日志用异步方式处理减少同步等待。另外要提醒一下undo_log表的维护。AT 模式下每次成功提交后日志会被删除但极端情况下仍有残留长期下来表会越来越肥。建议基于log_created字段定期清理超过几天的记录避免回滚时扫描大表拖慢性能。5. 结尾的一点私人体会Seata 这套方案从我第一次部署到线上稳定运行前后踩了不少坑也成长了不少。如果让我给初次接触分布式事务的团队一个最简路径我会说先别急着在生产环境铺开而是把一个订单库存的极简 demo 完整跑通然后故意制造库存不足和超时场景亲眼看到数据回滚再考虑接入真实业务。分布式事务框架的最大价值恰恰不是你正常流程里能感受到的而是在异常出现时能不能把账给彻底抹平。另外也想多说一句Seata 不是银弹。AT 模式虽然省事但全局锁的并发上限是硬约束TCC 虽灵活但三套接口的开发量和隐性 bug 并不适合小团队莽撞使用。我个人在实际操作中的体会是先把业务链路中的核心强一致节点梳理清楚能用消息队列异步削峰的尽量异步化只对真正需要同步保证一致性的那几步引入 Seata效果远比“全链路一把梭”稳妥得多。把这个框架当成一件趁手但需要敬畏的工具它才能帮你走得更远。