
最近在处理项目里分布式事务的时候又把 Seata 从头到尾捋了一遍。说实话Seata 的安装步骤看着简单但实际落地时版本、注册中心、事务分组这些环节没弄明白很容易一个服务都起不来。这篇就基于我自己的实操经验把 Seata 的安装步骤、TC/TM/RM 三个核心角色的工作原理以及对 AT 模式的理解一次性讲清楚适合刚接触分布式事务、正准备把 Seata 接入 Spring Cloud 或 Dubbo 项目的朋友参考。1. 装Seata之前先把版本和部署形态定下来很多人上手 Seata 的第一动作就是下载压缩包然后启动结果启动日志一片红。根因多半不是操作问题而是没有在动手前想清楚两件事版本配套和部署形态。1.1 版本如何跟Spring生态匹配Seata 的版本迭代非常快1.4.x、1.5.x、1.6.x、2.x 之间的配置结构和行为差异很大。以我目前用的比较多的 1.5.x 和 1.6.x 为例服务端和客户端的核心配置文件已经从早期的file.conf加registry.conf变成了以registry.conf为主、部分配置下沉到注册中心的形态。2.x 之后配置方式又进一步收敛很多原本写在本地文件的配置项需要放到 Nacos 等配置中心里。这里必须记住一个铁律server 端和 client 端的 Seata 版本必须一致或者至少是同一个大版本内的小版本差异。我在项目里见过 client 用 1.4.2、server 用 1.6.1结果 TM 注册全局事务时反复超时日志里全是奇怪的 io 异常。后来把两边统一到 1.5.2问题消失。和你项目里 Spring Boot、Spring Cloud Alibaba 的版本也要一起考虑通常 Spring Cloud Alibaba 的版本说明里会列出推荐的 Seata 版本优先按那个组合来。1.2 单机测试与生产高可用选File还是DB存储Seata 服务端也就是 TC负责维护全局事务状态这些状态必然要落盘。TC 的存储模式有file和db两种。file 模式全局事务、分支事务、全局锁信息都写到一个本地文件里。优点是零依赖解压就能跑适合本地开发和功能验证。缺点是不能集群因为多个 TC 节点无法共享同一个文件状态。db 模式把事务状态和锁信息写入数据库表支持多个 TC 节点组成集群生产环境基本都是这个方案。所以你在动手前先问自己一句这次是本地学原理还是直接奔着生产环境去如果只是跑通 demofile 模式完全够用如果是要上生产直接把 db 模式配好免得后面二次改造。2. TC服务端安装解压、配registry.conf、把8091端口跑起来TC 就是 Seata 服务端安装过程分四步下载解压、配置 registry.conf、按存储模式初始化脚本、启动验证。2.1 下载与目录结构从 GitHub 的 Seata Releases 页面下载对应版本的压缩包比如seata-server-1.5.2.tar.gz。解压后核心目录如下目录/文件作用bin/seata-server.shLinux 启动脚本bin/seata-server.batWindows 启动脚本conf/registry.conf注册中心与配置中心定义最常见的坑就出在这里conf/application.yml部分版本中 TC 的本地配置如存储模式、端口等logs/seata-tc.log启动和运行日志排查问题的第一现场2.2 registry.conf里的关键配置registry.conf分两大块registry和config。先看一段最常见的本地开发配置registry { type file file { name file.conf } } config { type file file { name file.conf } }registry.type file表示不接入注册中心客户端通过配置的地址直连 TC。这是最简单也最适合学习的形态。生产环境一般会换成registry { type nacos nacos { application seata-server serverAddr 127.0.0.1:8848 group SEATA_GROUP namespace } } config { type nacos nacos { serverAddr 127.0.0.1:8848 namespace group SEATA_GROUP } }这里我特别提醒一点registry和config的 type 可以分开配。比如你注册中心用 Nacos但配置还是想放在本地文件那registry.type nacos、config.type file也是允许的。刚开始不要图省事全走 Nacos 配置中心否则配置项加载不全会遇到一堆难排查的诡异问题。如果是 1.4.x 及之前的版本file.conf里还会有service段和store段。1.5.x 开始 TC 端把store配置挪到了conf/application.yml注意区分版本差异别拿旧教程硬套。2.3 DB存储模式初始化脚本如果用 db 模式需要提前建好数据库和表。老版本建两张表加一张锁表global_table、branch_table、lock_table新版本还有distributed_lock表。脚本在 GitHub 仓库的script/server/db/目录下按你用的数据库选。以 MySQL 为例核心建表语句大致是CREATE TABLE IF NOT EXISTS global_table ( xid VARCHAR(128) NOT NULL, transaction_id BIGINT, status TINYINT NOT NULL, application_id VARCHAR(32), transaction_service_group VARCHAR(32), transaction_name VARCHAR(128), timeout INT, begin_time BIGINT, application_data VARCHAR(2000), gmt_create DATETIME, gmt_modified DATETIME, PRIMARY KEY (xid), KEY idx_gmt_modified_status (gmt_modified, status), KEY idx_transaction_id (transaction_id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;表结构不用死记直接执行官方脚本即可。然后修改 TC 的存储配置store: mode: db db: datasource: druid dbType: mysql driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai user: root password: 123456 minConn: 5 maxConn: 20 globalTable: global_table branchTable: branch_table lockTable: lock_table queryLimit: 100注意dbType要和实际数据库一致MySQL 8.x 用com.mysql.cj.jdbc.DriverMySQL 5.x 则用com.mysql.jdbc.Driver。2.4 启动验证Linux 下启动cd /opt/seata/bin ./seata-server.sh -p 8091 -h 127.0.0.1 -m file-p指定端口默认就是 8091-h指定对外注册的 IP多网卡机器上这个参数特别重要-m可以临时指定存储模式file 或 db但要注意它和配置文件同时存在时以命令行为准。启动后观察logs/seata-tc.log出现类似下面这行就说明 TC 起来了[main] ico.seata.core.rpc.netty.NettyRemotingServer: Server started ...再验证端口是否监听ss -lntp | grep 8091如果端口没起来常见原因包括8091 被占用、application.yml里端口配置与启动参数冲突、DB 连接失败。日志里基本都会给出明确线索养成先看日志的习惯能节省大量排查时间。3. 客户端接入依赖、事务分组和那一个注解服务端跑通只是第一步真正让 Seata 生效的是客户端接入。客户端要做的事情说起来很简单引入依赖、配置事务分组、在业务方法上加GlobalTransactional。3.1 客户端依赖版本对齐以 Spring Cloud Alibaba 体系为例通常在pom.xml里引入dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId version2021.0.1.0/version /dependency这个 starter 会传递引入 Seata 的 client 端依赖。但我要强调传递版本不一定会和你的 server 端一致最好显式覆盖 Seata 版本dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.5.2/version /dependency这样版本可控排查问题时少一个变量。3.2 tx-service-group与vgroupMapping这是整个接入过程中最容易被忽略、也最容易出问题的概念。Seata 客户端并不是直接配置 TC 的地址而是先定义一个事务分组再通过映射找到 TC 集群。理解方式很简单事务分组像一个逻辑名字vgroupMapping 像一张路由表它告诉你逻辑名对应的 TC 集群在哪。在 Spring Boot 的application.yml里seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: file file: name: file.conf service: vgroup-mapping: my_test_tx_group: default grouplist: default: 127.0.0.1:8091tx-service-group是业务方使用的分组名vgroup-mapping.my_test_tx_group default表示这个分组对应名为default的 TC 集群grouplist.default则是具体地址。如果使用 Nacos 注册中心就把registry.type改成nacos并确保服务端的application名称、group 和客户端一致。很多人报no available service错误就是vgroup-mapping没配、或者客户端和服务端的 group 不一致导致的。这个排查思路后面我会单独讲。3.3 业务方法上加GlobalTransactional配置完成后在需要开启分布式事务的入口方法上标注注解Service public class OrderServiceImpl implements OrderService { Override GlobalTransactional(rollbackFor Exception.class, timeoutMills 30000) public void createOrder(OrderCreateDTO createDTO) { // 1. 创建订单 orderMapper.insert(buildOrder(createDTO)); // 2. 调库存服务扣减库存 stockFeignClient.deduct(createDTO.getProductId(), createDTO.getCount()); // 3. 调账户服务扣减余额 accountFeignClient.decrease(createDTO.getUserId(), createDTO.getAmount()); } }GlobalTransactional是 TM 的入口方法开始时会向 TC 发起全局事务注册拿到全局事务 XID方法正常返回则提交全局事务抛异常则回滚全局事务。有一点必须注意rollbackFor默认只捕获 RuntimeException如果业务抛的是 checked exception不写rollbackFor Exception.class是不会触发回滚的这个细节我在项目里踩过不止一次。4. TC、TM、RM三方协奏一次全局事务的完整生命周期安装跑通以后理解原理最关键的就是把 TC、TM、RM 三个角色在每一次全局事务中的分工看清楚。我用一个实际的下单扣库存场景来走一遍完整链路。4.1 三个角色各自的职责角色全称位置职责TCTransaction Coordinator 事务协调者独立部署的 Seata 服务端维护全局事务和分支事务的状态协调全局提交或回滚管理全局锁TMTransaction Manager 事务管理器业务发起方通常是被GlobalTransactional标注的方法所在服务向 TC 申请创建全局事务生成 XID根据业务执行结果向 TC 发提交或回滚请求RMResource Manager 资源管理器每个参与分布式事务的业务服务订单、库存、账户管理分支事务向 TC 注册分支执行本地 SQL 并记录回滚日志响应 TC 的提交或回滚指令一句话记忆TM 是发起者TC 是协调者RM 是执行者。三者的交互全部通过 netty 通信完成和具体的 RPC 框架没有关系。4.2 XID如何跨服务传递全局事务创建后TC 会返回一个全局唯一的 XID格式通常是IP:端口:事务ID例如192.168.1.10:8091:987654321。这个 XID 是整个分布式事务的纲领必须跟着调用链一路传递到每个参与方。跨服务传递的实现依赖各 RPC 框架的拦截器或过滤器Spring Cloud OpenFeign 场景Seata 提供SeataFeignInterceptor把 XID 塞进 Feign 请求头的TX_XID字段中。Dubbo 场景通过 Dubbo 的隐式参数传递机制把 XID 放入RpcContext。MQ 场景需要在生产者发送消息前将 XID 写入消息头消费者接收后取出并绑定到当前事务上下文。如果发现下游服务报Could not found global transaction xid十有八九是 XID 没有正确传递优先检查服务之间的调用方式是否被 Seata 的拦截器覆盖到了。4.3 成功与失败的两种终点完整时序分两条路径全部成功路径TM 调用 TC 的globalBeginTC 创建全局事务返回 XID。服务 A如订单服务执行本地业务 SQLRM 通过数据源代理在同一个本地事务里记录 undo_log然后向 TC 注册分支事务获取分支事务 ID。此时本地事务提交但全局事务并未提交。服务 A 通过 RPC 将 XID 传给服务 B如库存服务服务 B 重复上述 RM 流程。所有分支注册完成后TM 调用 TC 的globalCommit。TC 更新全局事务状态为提交通知所有 RM 异步删除各自的 undo_log。失败回滚路径前四步同上。服务 B 执行本地 SQL 失败或后续业务抛出异常RM 向 TC 上报分支失败。TM 收到异常后调用 TC 的globalRollback。TC 更新全局事务状态为回滚通知所有 RM 使用 undo_log 反向补偿把 beforeImage 记录的数据还原回去。全部 RM 回滚完成后TC 删除全局事务记录。这里有一个很多人误解的点一阶段本地事务是已经提交的物理数据已经变了。Seata 并不是靠延迟提交来保证一致性而是靠先记录回滚日志 二阶段反向补偿这就是它叫 AT 模式Automatically Transaction的原因。5. AT模式底层机制undo_log和全局锁如何兜底AT 模式是 Seata 最核心也是使用最广泛的模式。它的本质是对业务 SQL 做自动补偿不需要侵入业务代码只在框架层操作数据源。理解 AT 模式核心就两个东西undo_log 和全局锁。5.1 一阶段业务SQL和undo_log一起进本地事务在 Seata 管理的数据源中业务 SQL 的执行流程是解析 SQL生成beforeImage执行 SQL 前查询出即将被影响的数据行记录其原始值。执行业务 SQL本地数据变更。生成afterImage执行 SQL 后再查询这些数据行的当前值记录变更后的值。将 beforeImage、afterImage、XID、分支事务 ID 等信息写入业务库的undo_log表。把业务 SQL 和 undo_log 插入放在同一个本地事务里提交。向 TC 注册分支事务并申请全局锁。5.2 二阶段提交多数时候只是删一条日志全局提交时TC 通知各 RM可以提交。RM 收到指令后发现一阶段已经提交过了所以二阶段提交实际只需要异步删除 undo_log。这也是 AT 模式性能优于传统两阶段提交的地方绝大多数情况下第二阶段几乎没有成本。5.3 二阶段回滚镜像校验加反向补偿回滚才是 AT 模式的精彩之处。流程是RM 收到回滚指令定位到对应 XID 的 undo_log。读取出 beforeImage 和 afterImage。脏写校验把当前数据库中的实际数据与 afterImage 比对。如果一致说明一阶段之后没有其他事务改过这行数据可以安全回滚如果不一致说明发生了脏写此时回滚会失败并抛出异常需要人工干预。校验通过后根据 beforeImage 构造反向 SQL。insert 变 deleteupdate 变 update 回去delete 变 insert 回去。执行反向 SQL 后删除 undo_log 记录。5.4 全局锁避免脏写既然一阶段本地事务就提交了那怎么防止另一个全局事务读到中间状态靠的是全局锁。TC 会为每个分支事务涉及的主键记录或索引记录维护一把全局锁。RM 在提交本地事务后必须向 TC 申请到对应数据的全局锁才能继续否则会阻塞等待。全局锁的粒度精度到行级由 TC 统一管理。举个例子事务 A 对商品 id100 的库存执行了 update并持有全局锁此时事务 B 也想 update 同一行RM 会先去 TC 检查锁发现被 A 持有就进入等待。A 回滚后释放锁B 才能继续。这个机制确保了全局事务之间的隔离性避免两个事务同时修改同一批数据造成不可控的结果。全局锁和数据库本地锁的关系可以这样理解数据库本地锁保证单库内并发安全全局锁保证跨库场景下多个库之间的写操作有序化。6. 安装与联调中真正值得记住的坑Seata 的原理书面上都能查到真正让人头疼的是安装和联调中的细节。挑几个我实际踩过、且复现率极高的坑按排查思路写清楚。6.1 事务分组配置不一致服务端日志没报错但客户端找不到TC现象TC 已经启动8091 端口正常。客户端启动也不报错但第一次调用GlobalTransactional方法时控制台报io.seata.common.exception.RpcException: Failed to get available server: list of TC is empty排查链路先确认客户端tx-service-group配置的值例如my_test_tx_group。再确认vgroup-mapping.my_test_tx_group映射到的集群名例如default。看grouplist.default是否配置了 TC 地址。如果用的 registry.type file这一步必须有如果用的 Nacos则要在 Nacos 上确认seata-server服务是否注册成功group 是否为SEATA_GROUP。检查服务端registry.conf里这个配置是否被 Nacos 覆盖有些版本 TC 启动时会从配置中心拉取配置并覆盖本地文件的映射关系。这个问题的核心不是连不上而是路由关系没对上。就像你知道了快递公司的总机电话但不知道怎么查分机号自然找不到人。这个坑的高发原因在于不同版本对事务分组相关配置项的命名和默认值有差异1.4 之前是spring.cloud.alibaba.seata.tx-service-group1.5 之后又兼容seata.tx-service-group配置迁移时容易漏。6.2 undo_log和XID相关的高频异常异常一io.seata.rm.datasource.undo.UndoLogException: xid ... not found回滚时找不到 undo_log。常见原因业务库没有建 undo_log 表或者 undo_log 表和服务表不在同一个库。注意 undo_log 必须在业务数据库中创建而不是建在 Seata 服务端的库中。因为 RM 是在执行业务 SQL 的数据源上写入回滚日志的如果表不存在一阶段写日志时就报错了根本走不到回滚那一步。异常二Data truncation: Data too long for column rollback_infoundo_log 表中的rollback_info字段默认长度可能不够当一次 UPDATE 影响的行数较多、镜像数据较大时JSON 序列化后超过列长度。解决方法是把rollback_info改成MEDIUMTEXT或LONGTEXT。这个坑在批量更新场景下非常典型。异常三global table is empty或No available service全局事务表里没有记录要么是 db 模式初始化表没建全少了global_table、branch_table、lock_table要么是 TC 用的是 file 模式而你期望 db 模式。启动参数-m能临时指定但生产环境一定要确认配置文件里的store.mode是 db。6.3 从file存储切到DB存储的高可用改造本地用 file 模式跑通后上生产前改成 db 模式的步骤比想象中多准备好seata数据库和官方脚本里的三张表并确认排序规则是utf8mb4。修改 TC 端conf/application.yml里的store.mode为db正确配置url、user、password。如果使用了 Nacos 配置中心把新的store配置同步到配置中心否则客户端和服务端可能各自加载到不同的配置。重启 TC 后不仅要看 8091 端口还要看global_table是否有新记录写入。你可以手动调用一次带GlobalTransactional的接口然后SELECT * FROM global_table;如果能看到记录说明 db 模式真正生效了。如果是做集群多个 TC 节点连接同一个数据库且注册中心配置要一致客户端通过事务分组做故障转移。还有一个容易忽视的点db 模式下 TC 节点之间的事务状态是共享的但如果客户端用了 file 模式直连它只会连到grouplist里配的第一个地址一旦那个节点挂了并不会自动切换到其他节点。所以生产环境请务必使用注册中心不要图省事。我个人在实际操作中还有一个体会Seata 的安装和接入本身不难难的是对 TC、TM、RM 关系以及事务分组路由机制的理解是否足够深入。建议新接触的朋友不要在 demo 上花太多时间后就直接上生产而是亲手画一遍全局事务生命周期的时序图再对照日志把每一步的 XID 流转梳理清楚。这样即使以后遇到版本升级、配置重构也能靠原理解读新问题。最后再分享一个小技巧排查 Seata 问题时先在客户端和服务端同时打开 debug 日志重点关注RpcContext里的 XID、ConnectionProxy里的分支注册日志、以及 TC 端的globalStatus变化。日志里这三个关键点的顺序基本就能定位 90% 以上的安装和交互问题。