☰
Seata分布式事务全解析:核心架构、四大模式与实战排查
2026/10/9 3:05:12 网站建设 项目流程

聊分布式事务,绕不开 Seata 这个基础概念。但凡做过微服务拆分、跨库操作,或者面试被问到技术选型,大概率都会碰到它。Seata 是阿里开源的一套分布式事务解决方案,从最初的分支项目到成为 Apache 基金会顶级项目,如今已经是国内使用最广泛的分布式事务中间件之一。这篇文章我就把 Seata 的基础概念串起来讲一遍,适合刚开始接触分布式事务、准备接入 Seata 的读者,也适合那些用了一段 Seata 但一直没搞明白背后原理的人。作为系列第一篇,我会先把“它解决什么问题”“由哪些部分组成”“有哪些模式”“怎么搭起来”这几个基础问题讲透,后续再深入源码和实战场景。

1. 为什么需要 Seata:分布式事务的核心痛点

1.1 从本地事务到分布式事务的跨越

以前单体应用时代,一个订单操作基本都在同一个数据库里,service 方法上加个@Transactional,数据库自带的 ACID 就能保证所有操作要么全成功、要么全回滚。那时候“事务”这个词很少让开发头疼,因为所有事情都发生在同一个事务管理器、同一个数据源内部。

微服务拆分之后,事情就变了。一个下单流程往往要调订单服务、库存服务、账户服务,每个服务连接各自独立的数据库。原来的本地事务管不住跨库的数据变更,因为@Transactional只能保证单个数据源内的一致性。比如订单库插入订单成功,但库存库扣减失败,如果没有额外机制,就会出现“订单存在、库存没扣”的脏数据。这种跨服务、跨数据库的一致性问题,就是分布式事务要解决的。

我用一个生活化的类比来理解:你去超市买两件商品,一件用现金柜台结账、一件用自助机结账,两笔支付必须同时成功,否则不能出门。本地事务相当于只有一个收银台,能整体统一下单;分布式事务则相当于两个独立收银台之间需要有一个“协调员”,告诉两边一起完成或者一起取消。Seata 扮演的正是这个协调员的角色。

1.2 常见分布式事务方案对比

在 Seata 出现之前,业界处理分布式事务有很多路子,经典的有两阶段提交(2PC)、三阶段提交(3PC)、基于消息队列的最终一致性、补偿事务(TCC)等。它们各有侧重点,没有银弹。

方案核心思路优点缺点
2PC/3PC协调者统一询问参与者,先预提交再最终提交强一致性,模型清晰同步阻塞,协调者单点风险,性能差
TCC业务方实现 Try、Confirm、Cancel 三个操作灵活性高,性能较好侵入性强,开发量大,需处理空回滚和悬挂
MQ 最终一致性利用消息队列保证异步落库,失败重试解耦,性能好,可用性高最终一致,不是实时一致,需处理消息重复
Seata AT 模式基于数据库 undo_log 自动生成反向 SQL 回滚对业务几乎无侵入,自动补偿有额外锁开销,理解成本略高

这些方案的本质差异在于对 CAP 理论的取舍。分布式系统里,一致性、可用性、分区容错性不可兼得。XA 追求强一致,但会牺牲可用性和性能;TCC 和 Saga 则退到最终一致,以换取更高的吞吐和可用性。Seata 之所以被广泛接受,是因为它在一个框架里同时提供了 AT、TCC、Saga、XA 四种模式,让开发者能根据业务场景选择合适的一致性级别,而不是被迫绑定某一种实现。

2. Seata 的基础架构与核心组件

2.1 TC、TM、RM 三个角色

Seata 的整体架构可以拆成三个角色,这三个名字会贯穿你使用 Seata 的整个过程。

TC(Transaction Coordinator,事务协调者)是独立部署的服务端,负责维护全局事务的运行状态,协调所有分支事务的提交或回滚,也负责全局锁的管理。你可以把它理解成一个“总调度室”,所有分布式事务的决策都在这里发生。TC 在 Seata 里就是seata-server,启动后等待客户端接入。

TM(Transaction Manager,事务管理器)嵌入在业务应用里,负责开启全局事务、提交或回滚全局事务。它通常就是你在 Service 方法上见到的@GlobalTransactional注解。TM 的作用像是“发起人”,告诉 TC:“我要开始一个全局事务了”“我这个全局事务该提交了”“出错了,必须回滚”。

RM(Resource Manager,资源管理器)也嵌入在业务应用里,管理每个分支事务的资源。它负责注册分支事务、上报分支状态,并驱动本地事务的执行和回滚。RM 是“执行者”,每个参与全局事务的服务都会有一个对应的 RM。

三者协作的大致流程是这样的:TM 向 TC 发起全局事务申请,TC 生成一个全局事务 ID,也就是 XID;XID 会通过 Dubbo、Spring Cloud OpenFeign 或 RocketMQ 等组件在服务调用链中传播;每个被调用服务的 RM 拿 XID 向 TC 注册分支事务;业务执行完后,TM 向 TC 发起全局提交或回滚请求,TC 再通知所有 RM 执行对应操作。这套机制保证了跨服务操作有了统一的“大脑”。

2.2 全局事务的生命周期

一个 Seata 全局事务的生命周期,可以分成四个阶段:开启、传播、分支注册、提交或回滚。

开启全局事务很简单,在业务入口方法加上@GlobalTransactional注解即可。这个注解会被 Seata 的全局事务拦截器识别,在方法开始时通过 TM 向 TC 注册一个全局事务,然后拿到 XID。

@GlobalTransactional(timeoutMills = 30000, name = "create-order-tx") public void createOrder(OrderDTO order) { // 调用库存服务,扣减库存 // 调用账户服务,扣减余额 // 本地插入订单记录 }

XID 的传播是核心。如果调用链路上没有传递 XID,Seata 就不知道后续服务是同一个全局事务的一部分,分支事务也就无法注册。在 Spring Cloud 场景下,Seata 通过拦截FeignClient请求,在 HTTP Header 里加入TX_XID,下游服务从 Header 中取出并绑定到当前上下文。在 Dubbo 场景下则利用 RPC 隐式传参。如果自己实现 RPC,需要手动传递这个头。

分支事务注册发生在每个数据库操作被 Seata 数据源代理拦截时。Seata 会生成一个分支事务 ID,并向 TC 注册。这时候本地事务仍然由数据库自己的事务管理器处理,但多了一层“代理”用来记录前后镜像和做补偿。最终提交阶段,TM 会发送全局提交或回滚请求,TC 根据收集到的所有分支状态决定最终结果。如果任一分支失败,TC 通知所有分支回滚。这里还要注意超时控制,timeoutMills设置过长会让全局锁持有时间变长,影响并发;设置过短则容易把正常慢请求误杀,需要根据业务耗时来定。

3. 四大事务模式:AT、TCC、SAGA、XA

3.1 AT 模式:自动补偿的默认选择

AT 模式是 Seata 最引以为傲的模式,也是默认推荐模式。它的核心思想是自动生成反向 SQL 完成回滚,对业务代码侵入极小。你只需加上@GlobalTransactional,然后像写普通本地事务一样写业务逻辑,Seata 会在底层帮你完成分布式事务的协调。

AT 模式的执行过程可以拆成三步。第一步,RM 解析业务 SQL,生成执行前的数据快照(before image)和执行后的数据快照(after image),这两份快照会保存到数据库的undo_log表中。第二步,执行本地事务,提交数据库事务后,异步向 TC 报告分支状态。第三步,如果全局事务需要回滚,RM 根据undo_log中的前后镜像生成补偿 SQL,把数据恢复成原来的样子。

这种方案对开发者来说非常友好,但也有代价。AT 模式在全局提交或回滚前,相关记录需要持有全局锁,防止其他事务修改同一行数据。默认情况下,AT 模式的隔离级别是“读未提交”,也就是一个分支事务未提交前,其他事务可能读到未提交的数据。要避免这个问题,可以使用select ... for update或者@GlobalLock注解来强制走全局锁。

AT 模式适合大多数业务场景,尤其是那些已经写了大量普通本地事务代码、不太想为分布式事务改业务逻辑的团队。但要注意,它要求数据库本身支持本地事务,并且表必须包含主键。动态 DDL、无主键表、批量操作超过一定阈值都会让 AT 模式处理起来很麻烦。

3.2 TCC 模式:业务侵入但控制力强

TCC 模式把一次业务操作拆成三个阶段:Try 阶段做资源的预留和检测,Confirm 阶段做真正的业务提交,Cancel 阶段做回滚释放资源。比如账户服务扣款,Try 阶段先冻结一笔钱,Confirm 阶段把冻结变成真正扣款,Cancel 阶段把冻结的钱解冻。

TCC 模式的好处是性能高、灵活度高,因为不需要数据库的 undo_log,也不会长期持有全局锁。它把一致性控制权完全交给业务代码,适合那些需要精确控制资源状态的场景,比如账户余额、库存预留。但代价是开发量大,每个参与分布式事务的服务都要编写 Try、Confirm、Cancel 三个方法,还要处理空回滚和悬挂问题。

空回滚是指 Try 阶段由于网络超时等原因没有执行,但 Cancel 阶段却被调用,此时要能识别出“没有 Try 过”,并直接返回成功。悬挂是指 Try 方法在 Cancel 之后才到达,此时要拒绝执行 Try,避免资源被错误冻结。这两个问题在 TCC 模式里是绕不开的坑,通常通过事务控制表或者状态字段判别。

3.3 Saga 模式:长事务的编排与补偿

Saga 模式是一种长事务解决方案,核心是把一个大的分布式事务拆成一系列本地事务,每个本地事务都配有对应的补偿操作。如果后续某个步骤失败了,就按照相反顺序执行前面每一步的补偿逻辑。

Saga 有两种实现方式:一种是基于事件编排,也就是由 Seata 的服务编排引擎根据状态机定义来驱动流程;另一种是基于注解的方式,业务自己定义正向执行逻辑和补偿逻辑。Seata 的 Saga 模式通过状态机定义文件来完成整个流程的编排,适合业务流程特别长、中间步骤非常多、允许最终一致的场景。

和 AT 模式相比,Saga 不需要全局锁,不会长时间阻塞资源,吞吐量更高。但代价是一致性更弱,如果中间步骤较多,补偿链路也会很复杂。另外,Saga 缺少全局隔离性,其他事务可能读到中间状态的数据。选择 Saga 时,一定要确保补偿逻辑足够健壮,最好配合告警和人工介入机制。

3.4 XA 模式:数据库级强一致性

XA 模式是基于 XA 协议实现的分布式事务,它利用数据库本身对 XA 事务的支持,由 TC 充当协调者。Seata 从 1.2.0 版本开始支持 XA 模式,在DataSource层实现了对 XA 数据源的管理。

XA 模式的优点是强一致性,事务执行期间所有参与者都处于阻塞状态,直到全局事务结束,数据库层面能保证不会出现中间状态不一致。但它也有明显缺点:需要数据库原生支持 XA,对 MySQL 版本和存储引擎有要求;事务执行期间资源被锁定,并发能力会下降;参与者越多,阻塞时间越长,性能越差。

选型时可以参考下面这个简化表格:

模式一致性业务侵入性性能典型场景
AT最终一致(默认读未提交)低中多数微服务业务
TCC最终一致,灵活控制高高重要资源精确扣减
Saga最终一致,状态机驱动中高高长流程、多步骤业务
XA强一致低低强一致要求,低并发场景

4. 从零搭建一个 Seata 演示环境

4.1 部署 Seata Server

要跑通 Seata,第一步先把 TC 搭起来。目前主流方式是下载seata-server的发行包,我用的比较多的是 1.5.x 和 1.6.x 版本。解压后重点看两个文件:conf/application.yml和conf/registry.conf。

registry.conf决定 TC 的注册中心类型,可以使用 Nacos、Consul、Zookeeper 等,也可以直接用 file 模式。演示环境建议先用 file 模式,减少外部依赖。application.yml里的store.mode决定事务日志的存储方式,可以用 file 或者 db。生产环境建议用 db,并初始化数据库脚本:

-- 创建 seata 数据库,并执行以下三张表 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_status` (`status`) ); CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ); CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(32), `status` TINYINT NOT NULL DEFAULT 0, `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`) );

启动方式很简单,在 bin 目录执行./seata-server.sh -h 127.0.0.1 -p 8091。看到Server started日志后,TC 就绪了。

4.2 集成 Seata Client 到 Spring Boot

以 Spring Boot + MyBatis + Dubbo 或 OpenFeign 的典型微服务为例,每个参与分布式事务的服务需要做三件事。

第一件,在 pom 里引入 Seata 依赖。版本要和 seata-server 保持一致,否则会出现协议不兼容的问题。第二件,配置seata相关参数,重点是tx-service-group,它对应客户端的事务分组名称,客户端会通过这个分组找到 TC 地址。

seata: enabled: true application-id: order-service tx-service-group: my_tx_group registry: type: file config: type: file service: vgroup-mapping: my_tx_group: default grouplist: default: 127.0.0.1:8091

第三件,在每个业务数据库里创建 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), `rollback_info` LONGBLOB, `log_status` INT, `log_created` DATETIME, `log_modified` DATETIME, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

Seata 客户端在 Spring Boot 里会自动配置数据源代理,所以不需要手动替换DataSource。它通过DataSourceProxy拦截 SQL 执行,自动生成前后镜像。我建议在接入初期,把seata.client.log.exceptionRate调成 1,保证每个异常都输出完整日志,方便排查。

4.3 验证分布式事务

集成完成后,验证方式是让一次跨服务调用链路里出现“人为故障”。比如创建订单时,先调用库存服务扣减库存,再调用账户服务扣减余额,最后模拟账户服务抛出一个RuntimeException。正常情况下,这个异常会触发全局回滚,库存服务已经执行的库存扣减也要被撤销。

验证的关键是看数据库数据是否完全恢复原样。如果库存数量未变、订单表没有新增记录,说明全局回滚成功。如果库存扣了但订单没建成,这大概率是分支事务没有正确注册,或者 XID 没有传递到下游服务。

我遇到过很多次“回滚无效”的情况,最后排查下来基本都是 XID 没传。可以开启 Seata 客户端的 debug 日志,观察日志里有没有出现Register branch success和对应的全局事务 ID。没有 XID 时,下游服务会当作独立本地事务提交,自然无法回滚。

5. 实战中的常见问题与排查心得

5.1 全局事务不回滚的排查思路

很多人第一次接入 Seata 时都会栽在“不回滚”上。我的排查路径通常是这样:先看 TC 端日志,确认全局事务是否真的开启了;再看下游服务日志,确认分支事务是否注册成功;最后看异常是否被全局事务拦截器捕获。

一个常见坑是异常被业务代码捕获后没有往外抛,导致@GlobalTransactional感知不到失败,最终走了提交。Seata 默认只对抛出异常的方法触发回滚,如果你在 Service 内部 try-catch 后返回失败结果,它是不会回滚的。解决办法是把调用方的异常处理后重新抛出,或者直接用GlobalTransactionContext.reload手动回滚。

还有一个坑是表结构问题。AT 模式在生成反向 SQL 时,是根据undo_log里的前后镜像来执行的,如果表没有主键,或者字段包含 TINYBLOB 等特殊类型,回滚 SQL 可能生成失败,日志里会出现Cannot generate rollback SQL一类的错误。遇到这种情况,先检查表主键和数据类型的兼容性。

5.2 脏读与隔离性注意事项

AT 模式默认隔离级别是读未提交,这意味着在一个全局事务尚未提交时,其他事务可能读到该事务修改的中间数据。如果业务不允许这种脏读,需要额外处理。

常用办法是在需要强一致读的查询方法上加@GlobalLock,配合select ... for update使用。@GlobalLock会让查询操作尝试获取全局锁,如果全局锁被其他事务持有,就等待或失败;这样就不会读到未提交的数据。用的时候要注意,@GlobalLock只对当前本地事务生效,查询本身必须在一个事务上下文中,否则它不会触发全局锁逻辑。

我在实际项目中见过一种错误用法:只在方法上添加@GlobalLock,但方法内没有使用for update。这样其实无法从数据库层面锁定记录,全局锁也从不会注册,脏读照样发生。正确姿势是两者一起使用,缺一不可。

5.3 高并发下的性能陷阱

Seata 用起来方便,但不是没有代价。高并发场景下最容易遇到两个性能瓶颈:一个是全局锁等待,一个是 undo_log 表竞争。

全局锁的持有时间是整个全局事务从开启到提交或回滚的时间。如果某个分支事务执行时间很长,比如调用外部 API 或者批量处理几万条数据,持有锁的时间就会拉长,其他事务也必须等待,最终表现为接口耗时飙升。对这种问题,我的建议是尽量缩小全局事务的边界,把不需要强一致的耗时操作移到事务外,比如发短信、推消息这类异步动作,根本不应该放进分布式事务。

undo_log 表竞争体现在高频写入时,主键冲突、锁等待变多。可以从几个方面优化:定期清理已经结束的事务对应的 undo_log,避免表无限膨胀;给 undo_log 表设计合理的索引;必要时将 undo_log 和业务表放在同一实例,减少网络开销。

高并发的另一个注意点是分支事务的注册数量。如果一个全局事务调用了几十个服务,每个服务又包含多张表操作,分支数量会很庞大,TC 的内存和数据库压力都会增加。设计系统时,尽量把多个表的写操作合并到同一个服务内,用本地事务处理,减少需要被 Seata 管理的分支数量。

5.4 与 Spring Boot 版本、框架集成的兼容性问题

Seata 的客户端版本升级比较频繁,不同版本对 Spring Boot、Dubbo、OpenFeign 的适配行为有所差异。比如早期版本默认对 Feign 的拦截方式、Header 传递字段名可能不同,升级时需要对照官方文档调整配置。

实际排查中,我最常看到的问题是注册中心配置不一致。客户端配置的registry.type和服务端不一致时,客户端无法发现 TC 地址,日志会一直停留在no available service。这时候优先检查分组名tx-service-group和 vgroup 映射是否一致,不要只盯 IP 和端口。

另外,如果你的项目使用了多个数据源,需要确保所有需要参与分布式事务的数据源都走 Seata 的DataSourceProxy包装。如果有一个数据源漏了代理,这个数据源上的 SQL 就不会生成 undo_log,全局回滚时就会缺失分支,产生不一致。

写在最后的实际体会

Seata 的基础概念看起来并不难,真正上手之后才会发现很多细节藏在文档之外。我自己在最初接入时也踩过不少坑,最深刻的一点是:分布式事务不是“加个注解就万事大吉”,它要求你对调用链、数据源、异常处理甚至数据库表结构都有清晰的认知。刚开始使用 Seata 时,建议先在小流量、低并发的业务上完整跑一遍回滚流程,把所有日志关键字都看完,再逐步推广。后续我会继续更新 Seata 系列,深入讲一讲 AT 模式的源码实现、TCC 模式的空回滚和悬挂治理,以及 Seata 在生产环境下的性能调优,这些都是真正能让你从“会用”进化到“用好”的关键内容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询