做后端开发久了,迟早会撞上分布式事务这堵墙。本地事务靠数据库的ACID就能搞定,一旦拆成微服务,跨库、跨服务的原子性就成了绕不开的难题。Seata 就是目前 Java 生态里最主流的分布式事务解决方案之一,由阿里巴巴开源,社区活跃度很高,很多生产项目都在用它。这篇博文我打算把 Seata 的安装步骤和核心工作原理一次性讲透,重点说说 TC、TM、RM 这三个角色到底各管什么事,以及我在实际部署中踩过的坑。
这篇内容适合正准备在项目里引入 Seata、或者已经引入但遇到问题不知道怎么排查的读者。我会先讲整体设计思路,再拆解安装流程,最后结合实操代码和常见问题给你一套可以直接抄作业的方案。
1. 整体设计思路:Seata 到底解决什么问题
1.1 从一次跨库下单说起
先举个最典型的场景:用户下单,订单服务写订单库,库存服务扣库存库,账户服务减余额。这三个操作分布在三个独立的数据库里,任何一个失败,其他两个都得回滚,否则就会出现“订单下了但库存没扣”或者“钱扣了但订单没生成”的脏数据。
本地事务在这里完全失效,因为你没法在一个事务里同时控制三个数据库的连接。两阶段提交(2PC)协议倒是能解决这个问题,但传统的 2PC 实现太笨重,协调者单点、同步阻塞、资源锁定时间长,在高并发场景下根本扛不住。Seata 的核心设计思路就是尽量规避这些毛病,用一套轻量级的、对业务侵入极小的方案来完成分布式事务的协调。
Seata 把整个分布式事务的协调过程拆成了三个独立角色:TC(Transaction Coordinator,事务协调者)、TM(Transaction Manager,事务管理器)、RM(Resource Manager,资源管理器)。这个拆分非常关键,它把“谁来发起事务”“谁来协调分支”“谁来执行分支”这三件事彻底解耦了,后面我会逐个分析。
1.2 Seata 与同类方案的对比选型
市面上能选的分布式事务方案其实不少,除了 Seata,还有 MQ 消息事务、本地消息表、TCC 框架(如 TCC-Transaction、tcc-transaction)、Saga 等。我梳理了一下实际选型时的主要对比维度,方便你结合自己的场景判断:
| 方案 | 数据一致性 | 侵入性 | 吞吐量 | 适合场景 |
|---|---|---|---|---|
| Seata AT 模式 | 最终一致性(默认) | 低,几乎无侵入 | 高 | 绝大多数跨库业务 |
| TCC | 强一致性 | 高,需要写 Confirm/Cancel | 中 | 资金类、对一致性要求极高的场景 |
| MQ 消息事务 | 最终一致性 | 中,需要额外消息表 | 高 | 异步解耦场景,订单 -> 通知 |
| Saga | 最终一致性 | 高,需编排状态机 | 中 | 长事务、流程复杂 |
如果你是第一次接触这个领域,我建议优先把 Seata 的 AT 模式吃透。它最大的优势在于对业务代码几乎无侵入,你只需要在入口方法上打一个@GlobalTransactional注解,Seata 会在底层自动帮你完成分支事务的注册、提交和回滚。相比 TCC 那种需要手写三段逻辑的方案,AT 模式的上手成本低太多了。
2. 核心原理拆解:TC、TM、RM 三者如何协作
2.1 三个角色的职责定义
先花点时间把角色理清楚,因为后面所有配置和排障都离不开这三个概念。我直接用人话解释:
- TC(Transaction Coordinator):事务协调者,独立部署的服务端进程。它负责全局事务的开启、提交和回滚的决策,是所有事务状态的“大脑”。通俗点说,就像一个裁判,掌握着比赛(全局事务)的最终判罚权。
- TM(Transaction Manager):事务管理器,嵌在业务代码里。它负责向 TC 发起全局事务,并告诉 TC 这个全局事务到底应该是 commit 还是 rollback。它就是业务方的“代表”,向裁判汇报情况。
- RM(Resource Manager):资源管理器,同样嵌在业务代码里。它管理每个分支事务,负责向 TC 注册分支、上报分支执行状态,并接收 TC 的提交/回滚指令来操作本地事务。RM 就是实际干活的“运动员”。
这三者的关系可以用一段简单的流程描述:TM 通知 TC 开启全局事务 -> 每个服务的 RM 在本地执行 SQL,执行完向 TC 注册分支 -> 所有分支都执行成功后,TM 请示 TC 提交全局事务 -> TC 逐个通知 RM 提交各自的分支事务;如果中间任何一步失败,TC 就通知所有 RM 回滚各自的分支事务。
2.2 AT 模式的自动回滚实现原理
AT 模式是 Seata 最核心的卖点,它的名字其实是“Automatic Transaction”的缩写。它实现自动回滚的关键在于两个辅助表:全局锁表和undo_log表。
当 RM 执行一条业务 SQL 时(比如UPDATE stock SET count = count - 1 WHERE id = 100),Seata 会做三件事:
- 在业务 SQL 执行前,解析 SQL,生成前后镜像(before image 和 after image)。注意,前镜像不是直接从数据库读的,而是根据 SQL 类型、主键,用 SELECT 查出来的。Seata 会先把这些镜像以 JSON 的形式记录到
undo_log表里。 - 执行业务 SQL,并获取全局行锁,防止其他事务并发修改同一行数据。
- 分支事务提交时,异步删除
undo_log里的记录。
如果后续某个分支回滚了,TC 通知 RM 回滚,RM 就根据undo_log里的前后镜像,生成一个反向 SQL(把 after image 改回 before image),执行它来恢复原数据。这就是为什么表结构里必须建undo_log表,很多初学者忘了建表,一触发回滚就直接报错。
这里有个非常精妙的设计:Seata 没有用传统 2PC 的“锁定所有资源直到事务结束”策略,而是用“先记录、后修改、再确认”的方式,把锁的粒度大大缩小了,从而在高并发下保持了较高的吞吐量。代价就是需要额外的存储和 SQL 解析开销,以及极端情况下可能出现的脏读——不过 Seata 通过全局锁尽量避免了这个问题。
2.3 全局事务的超时与异常处理流程
全局事务的执行不是无限期的,Seata 有默认的超时控制,默认超时时间是 60 秒。这里有个容易踩坑的地方:@GlobalTransactional注解上有个timeoutMills参数,你可以单独为某个业务指定超时时间,但很多人忽略了服务端file.conf里的timeout全局默认值,导致明明业务还在正常执行,却被 TC 强制标记为超时回滚了。
超时回滚的流程是这样的:TC 在超过timeoutMills后还没收到 TM 的提交/回滚请求,就会判定这个全局事务超时,主动通知所有相关 RM 进行回滚。这个机制保证了分布式事务不会无限期悬挂,但也要求你设置合理的超时时间,不能拍脑袋。比如说,一个包含文件处理、外部接口调用的业务,可能要 90 秒,但你全局默认 60 秒,那高延迟时就会莫名其妙回滚。我的建议是把全局默认值调大一些(比如 120 秒),再在个别长任务上单独加注解超时。
3. Seata Server 安装与环境准备
3.1 版本选择与下载建议
我强烈建议你先确认自己项目的 Spring Boot / Spring Cloud / 数据库版本,再去选 Seata 版本。这里有个常见的坑:不同版本的 Seata 配置文件差异很大,老版本用registry.conf+file.conf,新版本(1.4.0+)支持了application.yml配置,再到 1.5.0 之后配置结构又变了一次。如果你直接拿网上旧教程的配置套新版本,大概率起不来。
目前生产环境用得比较多的稳定版本是 1.6.1 和 2.x 系列。就我的实测体验,1.6.1 资料最多、坑最少,2.0.0 换了配置文件结构(拆分成了application.yml和数据库脚本集成),配置管理方式更清晰了。如果你是用 Spring Cloud Alibaba,强烈建议对照版本对应关系表,否则客户端和服务端版本不一致,会遇到各种奇怪的分支注册失败问题。我个人目前生产环境用的是 Seata Server 1.6.1 + Seata Client 1.6.1,Spring Cloud Alibaba 2021.0.4.0,跑了一年多很稳定。
下载 seata-server 时注意两个发行包:seata-server-XXX.tar.gz是 Linux 的,seata-server-XXX.zip是 Windows 的。解压后的目录结构很简单,你需要的重点就两个子目录:bin(启动脚本)和conf(配置文件),后面我会直接围绕它们讲。
3.2 Nacos 注册中心与配置中心集成
Seata Server 启动后需要知道去哪注册自己、去哪读取配置,这就是conf下配置文件的用途。注册中心和配置中心可以选 Nacos、Consul、Etcd、Zookeeper 等,国内项目用 Nacos 的占比最高。我先讲 Nacos 路线,因为它的部署成本最低、界面友好。
首要前提:你有一个可访问的 Nacos,不管是本地开发还是服务器生产环境。然后要让 Seata Server 使用 Nacos,需要改两个地方:
- 在 Nacos 上创建 Seata 专用的命名空间(可选,强烈建议),避免和其他配置混在一起。
- 修改 Seata Server 的配置文件,把
registry.type改成nacos,并填上 Nacos 地址、命名空间、集群名等。 - 在 Nacos 配置列表里创建
seataServer.properties配置文件,把数据源、事务相关配置集中放进去。这样后续修改配置就不用再去服务器上改文件了,维护成本低很多。
需要特别留意的是,从 1.4 版本开始,Seata Server 的配置中心默认可以关闭,如果你不想用配置中心,可以把config.type设为file或留空。但生产环境我建议还是用 Nacos 统一管理,毕竟一旦有多台 Server,手改配置文件会把人逼疯。
3.3 初始化数据库脚本与全局配置
不管用哪种注册中心,有一个步骤绝对绕不开:初始化全局事务会话和锁记录所需的数据库表。Seata Server 在运行时需要把全局事务会话、全局锁等信息持久化到数据库里,所以你得先建库、建表。
在你的 MySQL 实例上新建一个数据库(比如seata),然后执行 Seata 安装包conf目录或源码script/server/db/mysql.sql中的脚本。这个脚本会创建global_table、branch_table、lock_table三张表,分别用于存储全局事务会话、分支事务会话和全局锁记录。
然后,业务数据库里每个涉及分布式事务的库,都必须额外建一张undo_log表。这张表前面已经说过,是 AT 模式实现回滚的核心。不同数据库类型有对应的建表脚本,script/client/sql/mysql.sql里面就有。注意:这步是更容易忽略的,很多人只初始化了服务端三张表,忘了业务库里要建undo_log,等到真正跑回滚时才发现数据库表缺失。
初始化完表之后,启动 Seata Server。Linux 下到bin目录执行sh seata-server.sh -p 8091 -h 127.0.0.1,Windows 下执行seata-server.bat。参数含义后面我细说,这里先记住默认端口 8091。启动后观察日志,如果出现“Server started”之类的字样,说明服务端好了。常见启动失败的原因就那么几个:端口被占用、数据库连不上、Nacos 没配好。我在第 5 章会单独开一节集中讲排障。
4. 客户端接入:Spring Boot 项目整合 Seata
4.1 Maven 依赖引入与版本锁定
服务端就绪后,客户端接入的流程就比较机械化了。我这里以 Spring Boot + Spring Cloud Alibaba 为例,因为这是目前最主流的搭配。
首先,在父工程的pom.xml里引入 Spring Cloud Alibaba 的 BOM(Bill of Materials,依赖版本管理)。BOM 的好处是帮你把 Seata 相关依赖的版本统一锁定,避免你自己去纠结什么版本和什么版本匹配。
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.4.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后在各个需要分布式事务的子模块里引入 Seata Starter:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>注意:spring-cloud-starter-alibaba-seata会自动引入 Seata 客户端相关依赖,不需要再单独加seata-all,加重复了反而容易版本冲突。如果项目里之前手动加过 Seata 依赖,建议先清理掉。
4.2 Nacos 注册与事务分组配置
依赖引入好了之后,就是告诉客户端去哪找 TC、怎么分组。这部分是新手最容易懵的地方,因为 1.4 之后的版本引入了“事务分组”的概念,很多人搞不清楚它的作用。
所谓事务分组,就是给一组服务定义一个逻辑名称(例如my_tx_group),然后客户端启动时拿着这个逻辑名称去配置中心查询真实的 TC 集群地址。这样做的好处是:如果 TC 的物理部署地址变了,不需要改每个客户端应用的配置,只需要改配置中心里的映射关系即可。理解了这个,你就不会写错配置。
在 Spring Boot 的application.yml里,核心配置如下:
spring: cloud: alibaba: seata: application: order-service tx-service-group: my_tx_group enable: true同时,你需要在 Nacos 的配置中心里维护一个service.vgroupMapping.my_tx_group=default的配置,告诉 Seata:“逻辑分组 my_tx_group 对应集群 default 里的 TC。” 如果集群配置为默认的default,那么 Seata Server 启动时的集群名也必须是default,否则客户端注册不上。
这里有个细节我强烈提醒:tx-service-group的逻辑名不要起得太随意,建议用业务线命名,比如order-tx-group、pay-tx-group。因为日志排查时你需要根据分组名快速定位是哪个业务集群。我之前踩过坑,全部用默认my_test_tx_group,线上排查问题时日志里全部一样,根本分不清是哪条链路。
4.3 使用 @GlobalTransactional 正确开启全局事务
客户端配置全部就绪,最后一步就是在业务入口方法上加注解。这里有个关键点:全局事务的入口应该在最外层调用的方法上,通常是 Controller 调用的 Service 方法或者 Feign 调用的入口方法,而不是每个内部子方法都加注解。
给个示例:订单服务创建订单后,需要 Feign 调用库存服务扣库存。
@Service public class OrderServiceImpl { @Autowired private StockFeignClient stockFeignClient; @GlobalTransactional(name = "order-create-tx", timeoutMills = 120000) public void createOrder(OrderDTO orderDTO) { // 本地事务:插入订单记录 orderMapper.insert(orderDTO); // 远程调用:扣减库存 stockFeignClient.deductStock(orderDTO.getProductId(), orderDTO.getCount()); } }@GlobalTransactional注解的name属性不是必填的,但我建议每个方法都起一个有意义的名字,否则日志里只显示一个全局事务 ID,排障时很难辨认是哪个业务。另外timeoutMills我习惯显式设置,而不是依赖默认值,这个习惯帮我少踩了很多超时回滚的坑。
子服务(库存服务)不需要加@GlobalTransactional,它只需要接入 Seata 客户端、注册到同一个 TC 上就行。Seata 会通过 RPC 调用链的上下文传递机制,自动把全局事务 ID 透传给子服务,子服务里的 RM 会自动加入这个全局事务。
4.4 业务库 undo_log 表的作用
前面提了业务库需要建undo_log表,这里展开说说。这张表在 AT 模式下是必须存在的,它记录了每个分支事务修改数据前后的镜像。一旦回滚,Seata 会从这张表读取镜像来生成反向 SQL,恢复原始数据。
表结构里有两个重要字段:branch_id(分支事务 ID)和undo_log(前后镜像 JSON)。有个容易忽略的点:undo_log表需要和应用的表放在同一个数据库里,因为 Seata 的事务回滚是在同一个本地事务里执行的,这样才能保证镜像记录和业务数据的一致性。如果你把undo_log放在另一个库,回滚时会因为跨库问题直接失败。
另外提一句:生产环境下建议定期清理undo_log废弃数据,否则这张表可能会积累大量历史镜像。虽然 Seata 在分支事务提交成功后会异步删除记录,但极端情况下(进程崩溃、网络中断)会有残留。我现在的团队就是写了个定时任务,每周清理一次超过 7 天的undo_log记录,实测对性能没有影响。
5. 常见问题与排查技巧实录
5.1 事务不生效的根因分析
“我加了@GlobalTransactional注解,但分布式事务就是不生效”,这是我被问得最多的问题之一。遇到这种情况,优先排查以下几件事:
- 是否引入了 Seata Starter?而且版本和服务端一致?
- 是否配置了
tx-service-group?客户端能不能从 Nacos 拉到配置? - 服务端 Seata Server 是否启动成功?网络能不能通?
- 业务入口和子服务链路,是否都在 Seata 的分布式事务上下文中?
还有一个很容易被忽略的坑:@GlobalTransactional注解如果加在私有方法上,或者加在类内部调用链路上(而非跨服务调用),它可能不会触发全局事务。Spring AOP 是基于代理的,类内部方法自调用不走代理,注解就会失效。我遇到过一个真实案例:用户把 Service 方法 A 调方法 B,B 上加了注解,A 没加,结果 B 作为内部调用根本没被代理,事务完全没开启,排查了一下午才发现是这个基础问题。这个问题在 Spring 事务里也存在,原理完全一样。
5.2 分支事务注册失败的场景分析
如果日志里频繁出现 “Could not found global transaction xid” 或 “Branch transaction register failed”,通常意味着子服务没有正确传递全局事务 ID。可能的原因有几个:
- 子服务没有接入 Seata 客户端,或者接入后还没连上同一个 TC。
- 使用的 RPC 框架版本与 Seata 不兼容,导致 xid 没有通过请求头透传。
- 服务端的注册中心配置和客户端不一致,导致客户端找不到正确的 TC 地址。
排查思路:先去子服务的日志里找是否有“transaction xid received”之类的记录。如果没有,说明 RPC 上下文传递断了。这时检查两件事:你是否用@GlobalTransactional注解在入口方法上?你的 Feign 拦截器有没有被 Seata 自动注册?spring-cloud-starter-alibaba-seata会通过SeataFeignClient自动在 Feign 请求头里追加 xid,但如果你自定义了 Feign 拦截器并直接覆盖了某些关键头信息,可能导致 xid 丢失。我第一次遇到这个问题时,就是在自定义拦截器里重写了整个请求头,把 Seata 的TX_XID给挤掉了。
5.3 超时导致的回滚问题
我参与过一个比较典型的生产事故:一批订单数据出现不一致,排查到最后发现是全局事务超时回滚导致部分分支没有被提交。因为业务里调用了外部接口,云上接口偶尔响应慢,整体耗时超过了默认 60 秒的全局事务超时阈值,TC 强制回滚了,但外部接口那边数据已经写进去了,产生了不一致。
这类问题的解决方案有几种:
- 给长耗时业务显式使用
timeoutMills,把超时时间调整到合理范围。 - 优化业务逻辑,减少事务内耗时不必要的外部调用,比如把非核心链路放到事务提交后异步执行。
- 区分“快事务”和“慢事务”,慢事务尽量用 TCC 或 Saga 模式,而不是 AT,避免长时间占用全局锁导致性能问题。
我在实际设计中,凡是涉及到外部接口同步调用的方法,都会先评估耗时分布,再设定timeoutMills。宁可设大一点,也不能为了“看起来严谨”而用自己的主观判断把超时时间设短。超市逻辑要留余量,这个道理在分布式事务里一样适用。
5.4 回滚失败与脏读问题的排查
最后一个容易出问题的点是回滚失败。回滚失败大多数情况下是数据校验失败,也就是说你在前后镜像对比时发现数据被其他事务修改了。Seata 在处理回滚时会比较当前数据和 after image 是否一致,如果不一致,说明数据被污染,它会选择抛出异常并记录Branch rollback failed日志。
这种情况往往是并发场景下全局锁失效导致的。排障时需要关注全局锁表lock_table里是否出现了异常的锁记录,以及所在表上的唯一索引、主键是否正确。Seata 的全局锁是依赖数据库资源实现的,如果业务表的索引设计不合理(比如缺少唯一索引),并发插入时会发生隐式锁冲突,导致回滚时镜像对比也失败。多查lock_table,多和 DBA 沟通表索引设计,能避免大部分这类问题。
关于脏读:AT 模式默认是读已提交隔离级别,但没有做读锁定。也就是说,在一个全局事务未提交时,其他事务可能读到这个事务修改过但还没提交的数据,这就需要业务在设计时考虑好隔离级别。如果确实需要更高隔离级别,可以通过配置开启SELECT FOR UPDATE或SELECT的全局锁辅助,但这会影响并发性能,需要权衡。
6. 综合建议与实践心得
玩 Seata 这几年下来,我最大的体会是:不要上来就在核心账务系统里引入 AT 模式,先在边缘业务跑通链路,再逐步扩大范围。账务系统对一致性极其敏感,一旦undo_log表缺失、全局锁冲突、镜像对比失败,处理起来都是大事故。我个人是在订单、库存这类非资金敏感业务里跑稳定了一年之后,才敢在积分、抵扣这种半资金场景引用的。资金核心链路目前还是用 TCC 自己把控,心里踏实一点。
还有一点补充:Seata 默认情况下不会限制branch_table和global_table无限制增长。高并发情况下,如果 TC 频繁开全局事务、短时间内大量分支注册,数据库里这两张表的数据量会快速上升。我的做法是监控这两张表的行数,超过阈值就清理,同时把 TC 的全局事务会话超时参数调短,确保会话不会长期支撑不释放。
最后再分享一个小技巧:用 Seata 做分布式事务,日志配置一定要单独开 logger 或者调高 Seata 相关包的日志级别。Seata 打的日志本来就不多,但关键信息(xid、branch id、回滚原因)都在里面。我习惯把io.seata的日志级别设为 DEBUG,配合链路追踪中间件,能非常高效地定位是哪个分支、哪条 SQL、哪种操作导致的问题。生产环境调回 INFO 也完全可以,千万别一股脑全拒掉。