资讯动态

Seata分布式事务实战:从环境搭建到AT/TCC模式接入与排错

发布时间:2026/10/23 12:12:40 来源:尧图企业网站定制
分布式事务这个话题相关原理文章已经多到泛滥但真正动起手来很多人还是会在接入阶段就卡住一两天。我见过不少团队理论讲得头头是道一上 Seata 就倒在配置、版本、XID 传递这些细节上。这篇是系列第三篇前两篇把 XA、TCC、消息事务这些基础理论都过了一遍这篇不再重复概念直接用 Seata 把一套下单流程从零落地走完整环境怎么搭、四种模式怎么选、代码怎么接、踩过的坑怎么排。如果你在做订单、支付、库存这类强一致场景或者正准备给微服务体系引入分布式事务方案这篇应该能帮你少走不少弯路。1. 先搞清楚你的场景到底该不该上 Seata很多人一听说分布式事务就条件反射地要上 Seata但分布式事务不是银弹先想清楚再动手比急着写代码重要得多。微服务拆分之后一次业务操作必然跨多个数据库比如一个下单动作订单库、库存库、账户库各自都要写数据。本地事务管不了跨库数据一致性全靠业务层面自己兜这就是分布式事务要解决的切口。Seata 的核心价值是给业务方提供一个全局事务框架事务发起方开启一个全局事务所有参与方作为分支事务加入最后由协调器统一决定全部提交还是全部回滚。它解决的是强一致性问题也就是要么全成要么全败的场景。而像积分发放、短信通知、日志上报这类最终一致就可以接受的流程用本地消息表或者消息队列做补偿就行没必要让它们挤进全局事务里白白拉长事务时间。再说直白点适合上 Seata 的场景有几个特征第一数据不一致会直接造成资损或者严重业务事故比如扣库存和扣余额必须同步成功第二流程链路不长通常涉及两到五个微服务第三事务内操作以数据库写为主没有太多耗时的外部调用。如果你的场景是长流程编排、需要人工介入审批这种那更适合 SAGA 或者干脆不用全局事务这一点在选型上要提前想清楚。2. Seata 核心架构与模式选型2.1 三个角色一次看懂Seata 的架构不复杂抓住三个角色就能理解全部机制。TC 是事务协调器也就是独立部署的 Seata Server它负责全局事务的开启、分支的注册和最终提交回滚的决策TM 是事务管理器通常就是业务入口那个服务负责向 TC 申请开启全局事务、在业务结束后发起全局提交或回滚RM 是资源管理器每个参与分布式事务的服务都是一个 RM它负责把自己那一分支的数据库操作注册给 TC并执行协调器下发的提交或回滚指令。整个协作流程是这样的TM 调 TC 开启全局事务TC 返回一个全局唯一 XIDXID 随着服务调用链一路传递到下游每个 RM 在执行本地 SQL 时向 TC 注册分支事务业务跑完后 TM 向 TC 发全局提交请求TC 再挨个通知每个 RM 提交或回滚分支事务。理解了这条主链路后面所有配置和排错都有方向。2.2 AT、TCC、SAGA、XA 怎么选Seata 一共提供四种事务模式很多人会在这一关纠结很久。XA 模式最正统利用数据库原生事务能力两阶段提交但同步阻塞严重性能损耗大现在生产环境已经很少直接用了。SAGA 模式适合长事务有点像编写逆向补偿流程状态机编排灵活但补偿逻辑全要手写对业务侵入也不小。实际生产里用得最多的是 AT 和 TCC。AT 模式是 Seata 的招牌最大的优点是无侵入。业务代码不需要改 SQL、不需要拆接口框架通过数据源代理自动记录数据变更前镜像和后镜像回滚时就靠这些镜像数据反向补偿。代价是它会多一步写 undo_log而且执行期间要持有全局锁并发冲突明显的场景要慎重。TCC 模式则是把 Try、Confirm、Cancel 三个方法由你手写业务可以在 Try 阶段做资源预留控制和性能都更好但你的接口设计和状态管理要额外扛两座山幂等和空回滚。我的建议是新增业务、以简单 CRUD 为主、并发冲突不严重直接上 AT涉及账户扣减、库存冻结这类资源预留型业务或者并发要求高优先考虑 TCC。3. 环境准备Seata Server 部署的关键决策3.1 版本选型是第一道坎Seata 的版本迭代蛮快不同版本配置方式差异很大服务端和客户端版本必须严格配套不然会出现各种莫名其妙的分支注册失败。我这边用 1.6.1 和 1.7.x 都跑过生产整体稳定。选版本时先看两件事你的 Spring Boot / Spring Cloud 版本是否在官方兼容列表里客户端和服务端是否同一个大版本。很多人在 1.4 时代照着旧博客配 registry.conf拿到新版 Server 上完全不起作用因为新版已经改用 application.yml 管理 server 端配置。先确认版本再找资料能省下一天时间。3.2 Server 端的两种部署形态Seata Server 有两种典型部署方式。测试环境为了方便可以直接单机 file 模式启动事务状态存本地文件注册中心也不用配但生产环境强烈建议用 Nacos 做注册中心、MySQL 存事务状态。事务状态如果只存在本地文件里Server 一重启未完成的事务就全部丢掉了这在生产上是不可接受的。用 MySQL 存储模式TC 的全局会话和分支会话都会持久化到数据库配合至少两个 Server 实例做高可用才能保证全局事务协调不单点。部署时先把 Server 的 conf 目录改清楚。核心配置有两块注册中心部分配置 Nacos 地址和服务发现存储部分配置 db 模式的连接串。启动命令类似 seata-server.sh -p 8091 -h 本机IP -m db后面几个参数含义网上资料很多我这里不展开但提醒一点-h 千万别配 127.0.0.1客户端从注册中心拿到的如果是回环地址跨机器调用就直接连不上这个问题非常隐蔽。3.3 客户端接入的最小配置客户端接入其实就三步引入依赖、配置事务分组和注册中心、确认数据源能被 Seata 接管。依赖一般用 seata-spring-boot-starter注意版本与服务端一致。事务分组的逻辑是客户端配置一个 tx-service-group比如 my_test_tx_groupServer 端配置这个分组映射到哪个集群客户端真正拿到的服务地址由 Server 端决定。这里最容易踩坑的是两边分组名不一致启动时控制台直接报 No available service排查半天注册中心最后发现就是大小写或者下划线差了一个字符。以下是一份可用的客户端 yml 模板seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP对应的 Server 端需要有这样的分组映射seata: service: vgroup-mapping: my_test_tx_group: default注意较早版本用 registry.conf 和 file.conf 配这些内容如果你网上搜到的是这两个文件名的教程先核对一下版本再照抄。4. AT 模式实战订单场景从零接入4.1 业务场景与表结构设计为了演示我们就做一个最典型的下单扣库存减余额流程。三个服务分别是订单服务、库存服务、账户服务各自连各自的库。订单服务的 createOrder 方法作为全局事务入口它先写订单表然后远程扣库存最后远程扣用户余额任何一个环节失败前面写的数据都要回滚。这个场景如果你在自己项目里复刻请把服务名、表名、字段名都替换成自己的。三个业务库的表结构很简单业务表之外每个库还必须额外建一张 undo_log 表这是 AT 模式回滚的地基。漏建这张表启动不报错但一执行事务就会在分支注册阶段卡住或者回滚时报错而且报错信息藏得比较深。表结构官方文档里有直接复制即可CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) 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, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;我每次接入新服务第一步做的事就是先确认这个库有没有 undo_log 表没有就先建别等联调期再被它坑一顿。4.2 事务入口与回滚边界全局事务入口只需要加一个注解这是 AT 模式对业务最友好的地方。入口方法的所属服务就是 TM方法执行前 Seata 会向 Server 申请全局事务并生成 XID方法正常返回就发起全局提交方法抛出异常就发起全局回滚。这里有个关键习惯注解上一定要写 rollbackFor Exception.class不要偷懒。默认情况下框架只对运行时异常回滚如果你的业务代码抛了个受检异常事务照样提交数据就永远一致不起来了。Service public class OrderService { Resource private StockClient stockClient; Resource private AccountClient accountClient; Resource private OrderMapper orderMapper; GlobalTransactional(rollbackFor Exception.class, timeoutMills 30000) public void createOrder(OrderDTO order) { orderMapper.insert(order); stockClient.deduct(order.getProductId(), order.getCount()); accountClient.deduct(order.getUserId(), order.getAmount()); } }看懂这段代码也就看懂了全局事务的边界它不是包住三个服务里的所有代码而是以这个方法为起点、以 RPC 调用链为路径把所有涉及的数据库操作纳入同一个全局事务。订单服务本地写订单表没什么难点真正关键的是库存和账户两个服务的调用怎么把 XID 带过去。4.3 XID 传递全局事务的生命线全局事务能不能成立取决于 XID 能不能跟着调用链传到每一个 RM。用不同的服务通信方式传递机制不一样。如果是 DubboSeata 的集成包会自动通过隐式参数传递如果是 Spring Cloud OpenFeign需要自己配一个 RequestInterceptor 把 XID 塞进请求头下游再取出来绑定到当前线程。引入 seata-spring-boot-starter 后新版本通常会自带 Feign 和 WebMvc 的拦截器配置但版本老一点就得手写建议不管自带不自带先看日志确认下游真的拿到了 XID再继续往下做。手写 Feign 拦截器很常规核心代码就这几行Configuration public class SeataFeignRequestInterceptor implements RequestInterceptor { Override public void apply(RequestTemplate template) { String xid RootContext.getXID(); if (xid ! null) { template.header(RootContext.KEY_XID, xid); } } }下游服务需要在业务方法执行前把请求头里的 XID 绑定到当前线程Seata 的 WebMvc 拦截器会做这件事。如果你用了 Spring Cloud Gateway还需要额外配置全局过滤器做同样的转发否则网关层就会把 XID 拦腰切断。排查分布式事务问题第一件事永远是确认整条链路每个节点的日志里有没有同一个 XID这个方法能筛掉一半的幻觉型 bug。4.4 AT 模式在数据层到底做了什么理解 AT 模式的数据层行为排错会轻松很多。当服务进入分支事务Seata 会代理你的数据源执行每一条业务 SQL 之前先查一次原始数据生成 before image再执行 SQL执行完再查一次生成 after image这三部分打包写进当前事务同库的 undo_log 表。全局提交时Seata 会把 undo_log 里的记录异步删掉全局回滚时则根据 before image 恢复数据恢复前还会对比当前数据和 after image如果发现不一致说明事务期间数据又被别的地方改过回滚会被判定为冲突这也是 AT 模式一边写一边并发写就难受的根本原因。所以 AT 模式适合的数据库操作一定是行级变更可追踪的、主键明确的像那种不带 where 条件直接 update 大表的 SQL会生成巨大的镜像数据性能和空间都扛不住。我通常会在代码评审阶段就把这种 SQL 拦下来要求必须带明确主键条件这既是为了数据库本身也是为 undo_log 的体积考虑。5. TCC 模式实战两个接口设计出三种事故5.1 为什么 TCC 仍然不可替代AT 模式再省事也替代不了 TCC。AT 模式主要在数据层做文章它并不知道你的业务语义只是在物理数据上做镜像对比和恢复。但有些业务比如电商的冻结库存预占余额要求 Try 阶段就预留资源Confirm 阶段真正扣减Cancel 阶段释放预留。这种语义 AT 做不了只能用 TCC 在业务层显式编码。另一个原因是性能和锁策略AT 的全局锁粒度更粗TCC 把资源固定在本地事务里锁的持有时间可控得多高并发场景优势明显。5.2 一个标准的 TCC 接口长什么样TCC 的核心是一个接口三个方法。以账户余额扣减为例Try 方法做资源预留就是把可用余额冻结一部分到冻结余额字段Confirm 方法做真正的扣减把冻结余额清掉、可用余额减少Cancel 方法做释放把冻结余额还原到可用余额。用注解把三个方法绑定在一起Seata 才能在二阶段正确回调对应的方法。LocalTCC public interface AccountTccService { TwoPhaseBusinessAction( name deductTcc, commitMethod confirmDeduct, rollbackMethod cancelDeduct ) boolean deduct( BusinessActionContext context, BusinessActionContextParameter(paramName userId) Long userId, BusinessActionContextParameter(paramName amount) BigDecimal amount); boolean confirmDeduct(BusinessActionContext context); boolean cancelDeduct(BusinessActionContext context); }实现类里最关键的不是正常流程而是对异常路径的处理。Seata 在二阶段回调时可能因为网络等原因多次重试Confirm 和 Cancel 必须幂等Cancel 也可能被调用在 Try 还没执行的情况下也就是空回滚问题极端情况下 Cancel 先到了Try 后到这就是悬挂。这三个问题不在接口设计阶段处理好上线后迟早出事故。5.3 空回滚、幂等、悬挂三个坑怎么填先说空回滚。一个分支事务在 Try 阶段没执行成功比如网络超时、服务宕机全局事务要回滚时就会调用 Cancel。这时候业务数据根本没有被 Try 动过Cancel 如果直接去释放资源就会产生错误的负数所以 Cancel 第一步要先判断 Try 到底执行了没有。我的做法是给冻结记录一个事务状态字段Try 执行前先落一条状态为预备的记录Cancel 里发现没有记录或状态不对就直接返回成功。再说幂等。二阶段回调没有做最多一次的保证Cancel 可能被连续调多次第一次已经把冻结余额释放了第二次再释放就重复扣了。所以在 Confirm 和 Cancel 里都要先查状态已经被处理过就直接返回。悬挂问题则是 Try 比 Cancel 晚到Cancel 已经把事务标记为已回滚Try 再执行就相当于凭空多出一个分支解决方式是在 Try 的最前面检查是否已经存在回滚标记有就直接跳过。如果你觉得这三个问题听起来绕我给你一个更直白的说法TCC 的本质不是写三个方法而是为你的资源状态机写一套完整的状态迁移加防重逻辑。状态字段至少要有初始、预留、已提交、已回滚四种所有方法进门前先核对当前状态是否允许迁移。把这个状态机设计清楚TCC 就成功了一大半。6. 常见问题与排查技巧实录Seata 的排错最怕的不是错误报得怪而是很多错误没有任何表面症状数据就这么悄悄不一致了。所以先说一条铁律接 Seata 的每个服务日志级别至少要把 seata 相关包开到 info线上保留全局事务开始、分支注册、二阶段提交回滚这几类关键日志。下面整理几个我实际工作中撞过的问题。现象可能原因排查手段与解决办法启动时报 No available service客户端事务分组与 Server 端 vgroup 映射不一致核对两边的 tx-service-group 和 vgroup-mapping注意空格和大小写调用链下游日志没有 XIDFeign/Gateway 拦截器缺失或异步线程未绑定检查 RequestInterceptor 和网关过滤器先确认服务间调用方式数据库没有生成 undo_log代理数据源未生效或表名错误确认业务库已建 undo_log 表查看 RM 分支注册日志全局锁等待超时多个事务并发修改同一行或事务跨度内耗时过长缩短全局事务耗时调整锁重试次数与超时参数回滚时出现 dirty data 校验失败分支事务期间数据被其他事务修改检查业务是否在全局事务外改了同一行数据或者 before 镜像生成时机不当除了表格里这些还有一个高频故障值得单独说seata-server 重启后客户端所有新事务都失败旧事务状态也容易丢失。这就是我前面说生产必须用 db 存储模式的原因。如果已经用了 file 模式排查未完成事务非常痛苦最省心的做法是尽快迁移到 db 模式并给全局事务表做好备份。还有一个我踩过好几次的坑是数据库驱动版本。Seata 的数据源代理会用到数据库元信息解析不同驱动版本对某些 SQL 的解析行为有差异尤其是 MySQL 8 的驱动和旧版本驱动在连接属性上容易出问题。接入失败时如果日志里出现 SQLException 或元信息相关报错先检查驱动版本是否和 Seata 版本兼容列表匹配再栽赃给业务代码。7. 性能调优与生产建议7.1 全局事务时长是性能的第一杀手AT 模式下全局事务从开启到提交或者回滚每个分支事务的一阶段提交都是在等 TC 的统一指令事务持有的资源锁时间被拉长了。多个并发用户同时操作用一行库存后到的请求会一直等待全局锁释放这比单库本地事务的锁等待时间指数级上升。所以性能调优的第一条准则不是调参数而是把全局事务的边界压缩到最小。能在事务外做的查询、预校验、数据组装一律放外面事务里只保留必要的写操作和 RPC。7.2 锁顺序和并发强度的提前预判如果多个服务在同一个全局事务里都要写数据尽量让它们访问资源的顺序在所有事务里保持一致否则容易出现交叉等待。比如所有下单流程都先扣库存、再扣余额、最后写订单不要某个分支反过来写。另一个思路是控制并发量依赖全局事务解决一致性的时候业务本身就不太适合承担过高的并发峰值必要的时候在入口做限流比事后调一大堆锁参数有效得多。7.3 别让无关业务挤进全局事务我见过一个项目把短信通知、优惠券发放、用户积分这些边缘业务全塞进一个全局事务理由是保证一定成功。结果下游短信服务偶尔超时整个订单流程跟着回滚用户被反复退款业务投诉直接炸了。正确的做法是只把真正要求强一致的写操作放进全局事务边缘业务用消息队列异步补偿让它的失败不影响主干。这个设计原则在选型那一节就提过这里再说一遍是因为它真的决定你线上是安心还是天天救火。再补充一点事务隔离级别相关的经验Seata 的默认隔离级别是读已提交AT 模式下通过全局锁实现写隔离但未提交的数据在二阶段前是可能被看到的。如果你的业务对中间状态敏感比如统计报表直接在事务执行期间去查了数据看到的就是还没最终确定的结果。这种情况要么加缓存控制查询时机要么接受最终一致的报表口径别指望 AT 模式给你数据库隔离级别的全部保证。最后聊一下扩展。这套 Seata 实战跑通之后后面可以继续折腾的方向不少比如把 seata-server 从单机升级成集群并接入生产环境告警比如给 TCC 分支事务增加可视化监控再比如把 AT 和 TCC 混合使用——订单主流程用 AT账户余额这类热点资源单独用 TCC。我个人的体会是分布式事务方案没有一劳永逸每次业务形态变化都可能需要对模式选择和事务边界重新做一次推演这一篇给的接入路径和排错思路是这套方法论里最核心的部分。至少我现在接到新项目第一反应不是哪个框架好用而是先按这套思路把场景、边界、模式定下来后面基本不慌。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑