☰
Seata AT模式详解:从分布式事务原理到undo_log回滚实践
2026/10/16 5:59:53 网站建设 项目流程

现代微服务架构里,分布式事务一直是绕不开的硬骨头。很多团队拆了单体、上了微服务,结果一个跨库操作的业务需求就把大家卡住了。SEATA分布式事务就是为解决这类问题而生的开源方案,而AT模式作为Seata中最常用、对业务侵入最小的事务模式,几乎是后端工程师入门分布式事务的第一站。这篇文章就基于“SEATA分布式事务——AT模式一”这个主题,把AT模式从原理到落地完整讲透:它到底怎么工作、为什么能无侵入地接管业务事务、核心的undo_log和全局锁机制是怎么回事,以及实操中你一定会遇到的坑。无论你是刚接触分布式事务的新手,还是已经踩过几个坑、想系统补全原理的中级开发,这篇都适合你花二十分钟通读一遍。

1. Seata是什么,AT模式解决什么事

1.1 先搞清楚分布式事务问题从哪来

先别急着看代码,得先理解分布式事务到底在解决什么问题。

单体时代,一个订单逻辑往往就是“扣库存、生成订单、更新账户”这三步,都在同一个数据库里,一个@Transactional注解就搞定。但在微服务架构下,订单服务、库存服务、账户服务各自拥有独立的数据库,原本一个本地事务被拆成了三个独立事务。这时就出现了一个经典问题:如果扣库存成功,但生成订单失败,库存已经被扣掉了怎么办?

这就引出了分布式事务的核心矛盾:多个参与者(各服务的数据库)之间如何保持数据一致。分布式事务就是用来协调多个独立资源,让它们要么全部成功,要么全部回滚,保证最终一致性。

业界常见的解决方案有很多,比如XA两阶段提交、TCC补偿、本地消息表、事务消息等。而Seata(Simple Extensible Autonomous Transaction Architecture)提出的AT模式,就是在此基础上进一步优化,目标是对业务代码侵入最小化——你几乎感知不到分布式事务的存在。

1.2 Seata在事务方案矩阵里的位置

如果给分布式事务方案分个梯队,大致可以这样看:

方案侵入性一致性性能典型场景
XA高,需要数据库支持强一致一般传统金融核心链路
TCC高,需要写try/confirm/cancel最终一致较好复杂业务、需要自定义补偿
本地消息表中最终一致好异步解耦场景
Seata AT低,几乎无侵入最终一致(默认读未提交)中等大多数微服务业务系统

Seata AT模式的核心卖点就是“无侵入”。它通过拦截SQL、生成前后镜像、利用undo_log实现自动回滚,业务代码里只需要加一个@GlobalTransactional注解即可。这也是它能在众多方案中脱颖而出的根本原因。

1.3 为什么建议你先学AT模式

如果你是第一次接触Seata,把AT模式作为第一个学习对象是最合理的路径。原因有三:

其一,AT模式与日常开发的贴合度最高。绝大多数微服务项目使用的都是关系型数据库和MyBatis/JPA这类框架,AT模式不要求你改写业务代码、不要求业务表额外增加逻辑字段,上手成本极低。

其二,AT模式的设计思路极具代表性。它通过“事务协调器+分支事务+全局锁”的模型,非常清晰地展示了分布式事务的三个核心角色:TC(事务协调器)、TM(事务管理器)、RM(资源管理器)。把这套角色模型理解透彻了,后面学习TCC模式、Saga模式都会轻松很多。

其三,AT模式的坑和优化空间比较大,踩过这些坑之后,你对数据库事务、锁机制、SQL解析的理解都会上一个台阶。这些知识是通用的,不局限在Seata这一个组件上。

2. AT模式的核心机制:一阶段、二阶段与全局锁

2.1 一阶段:业务SQL如何变成undo_log前后镜像

AT模式之所以能做到无侵入,是因为它把“记录回滚数据”这件事自动做了。在Seata的架构里,每个参与全局事务的本地事务称为“分支事务”,而记录回滚数据的载体就是一张名叫undo_log的日志表。

一阶段的处理流程可以这样理解:

当你的业务方法加了@GlobalTransactional注解发起全局事务后,Seata的RM(资源管理器)会拦截业务SQL。在真正执行UPDATE account SET balance = balance - 100 WHERE id = 1之前,Seata会先执行一次SELECT * FROM account WHERE id = 1,把当前这行数据的完整内容存为“前镜像”。

然后业务SQL正常执行,执行完成后再执行一次SELECT * FROM account WHERE id = 1,拿到更新后的数据作为“后镜像”。最后,Seata把前镜像、后镜像、业务SQL、全局事务ID、分支事务ID等信息拼接成一条JSON记录,插入到undo_log表中,并和业务SQL放在同一个本地事务里一起提交。

这就是一阶段的全部工作。注意一个关键点:业务数据和undo_log是在同一个本地事务里提交的。这保证了只要业务SQL提交成功,undo_log就一定存在;如果业务SQL失败回滚,undo_log也不会残留。这个设计非常巧妙,用本地事务的原子性保证了对回滚日志记录的可靠性。

2.2 二阶段提交与回滚:全局锁的写入规则

一阶段完成之后,TC(事务协调器)会发起二阶段。二阶段分为两种情况:全局提交和全局回滚。

全局提交时,因为业务数据已经提交成功,此时只需要把undo_log里的对应记录删除即可,不需要再做任何数据恢复操作。注意,这里删除undo_log是异步批量执行的,不会阻塞业务链路。

全局回滚时,Seata会从undo_log中读取前镜像,生成一条反向补偿SQL。例如原始SQL是UPDATE account SET balance = balance - 100 WHERE id = 1,那反向SQL就是UPDATE account SET balance = balance + 100 WHERE id = 1,然后执行这条补偿SQL把数据恢复原状。在执行补偿SQL之前,Seata还会做一次数据校验,用当前数据与后镜像做对比,确认这行数据在分支事务期间没有被其他事务修改过,否则不会盲目回滚。

这里涉及一个核心概念:全局锁。

Seata AT模式在写隔离上的策略是:在分支事务一阶段提交前,先申请全局锁。也就是说,如果两个全局事务同时要修改同一行数据,后到达的事务必须等待前一个全局事务释放全局锁,否则无法在本地提交。

这里有个容易混淆的点需要特别说明:全局锁是Seata层面管理的逻辑锁,不是数据库的行锁。它存储在Seata服务端的lock_table表里,以“资源ID(resourceId)+ 主键值”为维度标识一行数据。正因为是逻辑锁,所以它的排队等待机制、超时处理、与数据库事务的交互顺序,都要靠Seata Server端来实现。

2.3 写隔离和读隔离:这个设计经常被误解

AT模式的隔离级别,是很多人理解偏差的重灾区。

先说写隔离。因为有了全局锁,AT模式保证了两个全局事务对同一行数据的写操作是串行的。注意这里的串行指的是全局事务层面,而不是单纯的数据库层面。如果一个全局事务正在操作某行数据,另一个普通本地事务(没有加入全局事务)直接修改了这行数据,Seata是管不到的,这会导致undo_log中的数据校验失败,进而影响回滚的正确性。这是AT模式在实际应用中必须规避的场景。

再说读隔离。在默认配置下,AT模式是读未提交级别。什么意思?就是在一个全局事务执行过程中,如果事务A已经提交了本地分支事务(一阶段完成),但全局事务还没最终提交,那么此时事务B是可以读到事务A修改后的数据的。这在某些业务场景下会产生脏读。

要解决脏读问题,Seata也提供了SELECT FOR UPDATE语句的支持。当业务SQL使用SELECT ... FOR UPDATE时,Seata会在查询时尝试获取全局锁,如果获取不到就等待,从而避免了脏读。但这需要业务代码显式使用FOR UPDATE,并且性能会有一定损耗。

3. 从零搭建一个AT模式demo,环境与配置一次说清

3.1 依赖引入与配置项解析

纸上谈兵没意思,直接进入实操。我以一个典型的订单-库存-账户三个服务的模拟项目为例子,跑一遍AT模式全流程。

假设你已经有一个可运行的Nacos注册中心(Seata Server端也需要注册到Nacos),Seata Server 1.5.2以上版本已经启动,接下来改造你的业务服务。

Maven依赖是第一步:

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.2.5.RELEASE</version> </dependency>

注意版本兼容性。Spring Cloud Alibaba 2.2.5对应Seata 1.4.2,如果使用的是Seata 1.5.x,需要手动引入对应的seata-spring-boot-starter并排除默认版本。我试过比较稳的组合是Spring Boot 2.3.x + Spring Cloud Alibaba 2.2.5 + Seata Server 1.4.2,这套组合踩坑最少。

配置文件中需要指定事务组到TC集群的映射:

spring: application: name: order-service cloud: alibaba: seata: tx-service-group: my_test_tx_group seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP application: seata-server config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP service: vgroup-mapping: my_test_tx_group: default

这段配置的关键是tx-service-group和vgroup-mapping。事务组是客户端视角的概念,它可以通过配置中心映射到真实的Seata Server集群。如果你不在配置中心维护映射关系,也可以在本地配置文件里写死vgroup-mapping。很多人在这一步出错,因为配置文件里没有正确配置事务组,导致客户端找不到TC节点,启动后事务全部无法注册。

3.2 三个核心表结构交代清楚

Seata AT模式需要三张核心表,分别位于业务数据库和Seata Server对应的存储数据库里。

业务数据库中需要创建undo_log表:

CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization', `rollback_info` LONGTEXT NOT NULL COMMENT 'rollback info', `log_status` INT NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

这里有个细节:branch_id和xid的组合唯一键是必须的,Seata会根据这两个字段定位回滚记录。如果你的表结构与官方不一致,会导致回滚时找不到数据。

Seata Server端如果使用文件存储模式,会在其工作目录下自动创建file_store数据文件;如果使用数据库存储模式,则需要手动创建global_table、branch_table、lock_table三张表。早期的Seata版本还需要同时创建global_table等表,1.4.2版本的数据库模式建表脚本在官方源码的script/server/db目录下可以找到。

3.3 一个完整的转账demo跑通全流程

下面以最经典的“下单扣库存扣余额”为例。核心服务方法如下:

@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private AccountFeignClient accountFeignClient; @Autowired private StorageFeignClient storageFeignClient; @GlobalTransactional(name = "create-order", rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 本地插入订单数据 orderMapper.insert(order); // 2. 远程调用库存服务扣减库存 storageFeignClient.decrease(order.getProductId(), order.getCount()); // 3. 远程调用账户服务扣减余额 accountFeignClient.decrease(order.getUserId(), order.getAmount()); } }

就是这么简单,只有一个@GlobalTransactional注解,业务代码没有任何分布式事务的痕迹。createOrder方法执行时:

  • TM(事务管理器)向TC申请全局事务XID
  • 每个参与的服务在本地执行SQL时,RM自动注册分支事务
  • 所有分支事务都提交后,TM触发全局提交
  • 如果任何一步抛出异常,TC通知所有分支事务回滚

跑通这个demo之后,你可以尝试在账户服务里人为抛出一个异常,观察数据库里订单、库存、余额三个表的数据是否完整回滚。第一次跑通回滚时,那种“加了分布式事务真的能自动恢复数据”的体验,确实比看十篇原理文章都深刻。

4. undo_log表到底做了什么,回滚失败与排查实录

4.1 undo_log字段逐项拆解

很多人在初学AT模式时,只把undo_log当成一张“自动生成的日志表”,从没仔细看过里面的内容。其实这张表里的每一条记录,都是排错时最直接的线索。

拿一条实际记录来看:

{ "branchId": 5234567890123456, "xid": "10.10.10.10:8091:523456789012345", "undoItems": [ { "sqlType": "UPDATE", "tableName": "account", "beforeImage": { "rows": [ { "id": 1, "balance": 1000 } ], "tableName": "account" }, "afterImage": { "rows": [ { "id": 1, "balance": 900 } ], "tableName": "account" } } ] }

逐项拆解一下:

  • branchId:分支事务ID,用于标识该分支事务在全局事务中的唯一位置。
  • xid:全局事务ID,由“TC地址:端口:递增序号”组成,用于定位全局事务。
  • undoItems:回滚项数组,里面存放了SQL类型、表名、前后镜像数据。多条SQL在一个分支事务里执行,就会生成多个undoItems。
  • beforeImage:前镜像,即执行SQL前的数据快照。回滚时Seata会根据前镜像生成反向SQL。
  • afterImage:后镜像,即执行SQL后的数据快照。回滚前Seata会把当前数据与后镜像做对比,判断数据是否在事务执行期间被外部修改。

排查回滚问题时,我建议先查这张表。如果某个分支事务没有生成undo_log记录,说明一阶段就没走完,问题出在SQL拦截或本地事务提交阶段;如果undo_log存在但回滚失败,那就需要重点检查rollback_info里的数据校验逻辑。

4.2 回滚失败与脏读问题实录

我在实际项目中遇到过几次典型的回滚失败案例,每次复盘都很有价值。

第一个案例是脏数据导致镜像校验失败。某个定时任务在全局事务执行期间,直接通过数据库客户端手动修改了同一行记录。全局事务随后因异常触发回滚,执行反向SQL前做数据校验时发现当前数据与后镜像不一致,Seata会抛出BranchRollbackFailedException。这种情况下Seata默认会将这次回滚标记为“需人工干预”,不会强制覆盖掉脏数据。遇到这种问题,先用SQL查一下undo_log里该分支的记录状态,再结合业务日志确认是否有外部写入。

第二个案例是全局锁等待超时。在订单服务同时接收到大量相同商品的购买请求时,全局事务A还没提交,全局事务B就需要等待全局锁释放。如果等待时间超过了客户端配置的lockRetryInterval总超时时间,事务B直接抛异常失败。这种场景在高并发秒杀中很常见。解决办法不是无限调大超时时间,而是减少事务的持有时间——尽量把远程调用、耗时的非事务操作移出@GlobalTransactional方法体。

第三个案例是错用本地事务与全局事务混用。如果在一个@Transactional方法里调用了另一个@Transactional方法,并且内部方法也定义了自己的事务边界,就很容易出现“事务传播级别导致分支事务提前提交”的问题。这一点特别容易踩,团队里多个新人共同维护代码时尤其高发。建议在引入Seata的项目里,把@Transactional的嵌套使用规范写进开发约定里。

脏读问题在AT模式下也确实存在。默认隔离级别是读未提交,如果一个全局事务还没有全局提交,但其中某个服务的本地事务已经提交了,对外的查询就能读到这些“半成品”数据。如果你的业务有强一致性读需求,一种做法是在查询方法中使用SELECT ... FOR UPDATE,让Seata在查询时检查全局锁;另一种做法是接受最终一致性,在查询侧做数据标记,比如增加“状态字段”,事务未完成前标记为“处理中”,业务侧过滤掉这些记录。

4.3 网络、超时、事务悬挂等实际运维点

Seata整体是依赖网络通信的分布式组件,网络抖动几乎无法避免,所以客户端与TC之间的超时设置值得格外关注。

客户端侧的timeout默认是60000毫秒,建议根据业务实际耗时调整。如果你的全局事务里有大量慢SQL或者远程调用,把这个值调大一些是合理的,否则会出现“事务还没执行完但TC判定超时”的尴尬情况。但是不能无脑调大,因为TC侧可能因为持有全局锁时间过长,导致后续事务大面积阻塞。

事务悬挂问题也值得提一下。所谓“悬挂”,是指分支事务的一阶段提交成功,但TC没有收到注册请求或者收到了但处理失败,导致这个分支事务的undo_log始终残留。这个问题通常和Seata Server端压力过大、网络重发机制有关。如果在undo_log表里发现长期存在log_status=0且与任何活跃全局事务都不关联的记录,大概率就是悬挂事务。这类记录不影响业务正确性,但会占用空间,也说明系统出现过异常情况。排查时需要结合TC日志和客户端日志对时排查。

注意事项:不要在生产环境直接清理undo_log表中的异常记录,尤其是全局事务还没结束时。先确认对应的全局事务状态,确认已经处于结束状态再清理,否则可能破坏回滚数据链。

5. AT模式哪些场景用不了、哪些坑必须提前知道

5.1 适用场景与限制,一张表讲清楚

AT模式虽然好用,但不是什么场景都能无脑套用。我把适用场景和限制整理成了一张表:

场景是否推荐原因
跨库事务,各服务使用MySQL/Oracle等关系型数据库推荐AT模式本身就是为关系型数据库设计的
高并发秒杀、热点账户操作谨慎使用全局锁竞争激烈,容易超时
分库分表场景不支持无法解析路由后的真实表结构
非关系型数据库(Redis、MongoDB等)不支持AT模式目前只支持主流关系型数据库
事务内包含大量外部API调用不推荐长时间持有全局锁,影响整体并发
需要强一致读的业务谨慎使用默认读未提交,需要业务侧处理

这里面最容易被忽略的是分库分表。如果你的服务使用了ShardingSphere这类中间件做了分片,AT模式的SQL解析会失效,因为Seata解析出来的表名和实际物理表不一致,导致分支事务注册失败或者回滚错乱。我在某项目中就遇到过这个问题,最终退而求其次改用了TCC模式。

5.2 与TCC模式、Saga模式的选型对比

很多人在选分布式事务方案时,会在AT、TCC、Saga三者之间纠结。我的选型经验如下:

AT模式适合“业务逻辑简单、希望快速接入、对性能要求不极致”的场景。它最大的价值是快,开发成本几乎为零。但代价是SQL解析和镜像数据比对有额外开销,并且对数据库兼容性有依赖。

TCC模式适合“核心资金链路、业务逻辑清晰、需要自定义补偿语义”的场景。TCC需要你为每个业务操作编写Try、Confirm、Cancel三个方法,开发量大很多,但这些方法完全由业务掌控,补偿逻辑可以由你精确控制,性能上也比AT更优,因为没有全局锁的纠缠。

Saga模式适合“长事务、业务流程复杂、中间步骤可能持续很久”的场景。Saga的本质是把一个长事务拆成一系列本地事务,通过正向操作和反向补偿来达到最终一致。它的缺点是缺乏隔离性,需要业务侧自行处理脏读问题。

一句话总结:能用AT的地方优先用AT,AT搞不定的再考虑TCC或Saga。不要为了追求“高级方案”而徒增开发成本。

5.3 性能损耗,真实压测数据参考

最后聊聊性能。很多文章会说AT模式性能差,但“差”到什么程度,没有量化数据的话很难评估。

在一次模拟项目中,我对一个包含“插入订单+扣减库存+扣减余额”三个分支事务的全局事务做了压测。单服务本地事务(不接Seata)TPS大约在3200左右;接入AT模式单全局事务且无竞争时,TPS降到2700左右,性能损耗约15%;模拟20个并发同时对同一商品扣库存时,由于全局锁竞争,TPS下降到900左右,且伴随大量锁等待超时异常。

这个结果说明:在无竞争场景下,AT模式的性能损耗是可控的;但在热点数据高并发写入场景下,全局锁是真正的瓶颈。针对这个瓶颈,可行的优化方向包括:

  • 合并小事务:一个全局事务内尽量少的分支事务,减少锁持有范围。
  • 异步化非核心操作:把不需要强一致性的步骤移到事务外面。
  • 拆分热点:比如账户余额这类热点数据,设计多个子账户分散热点更新。

6. 个人实操体会与后续拓展建议

我前前后后在两个项目里接入过Seata AT模式,一个刚上线就遇到回滚失败问题,一个稳定运行了一年多。对比下来,最重要的体会是:AT模式本身不算复杂,但接入前一定要把业务场景的并发特征和事务边界想清楚。是不是有热点行竞争?分支事务里有没有外部调用?每个分支事务要不要自控超时?这些问题在代码里多花两小时梳理清楚,比上线后半夜看报警要强得多。

还有一个非常实用的小技巧:Seata提供了GlobalTransactionContext.reload(XID)这个方法,可以用来在异常处理中手动查询全局事务状态。我在排查问题时经常在全局事务方法的catch块里加上状态打印,把全局事务ID、分支事务数量、TC返回的状态码全部打出来,配合undo_log表里的数据和TC侧日志,绝大多数问题都能快速定位到具体环节。

另外提醒一句,Seata的版本迭代很快,从1.4到1.5到2.x,配置项和使用方式都有变化。如果你在网上搜到的配置片段和当前版本对不上,不要硬套,去官方文档确认一下当前版本的配置格式。我见过不少同事因为用了旧版配置导致Seata客户端连接不上TC,排查半天才发现是配置项名称变了。

如果你已经吃透了AT模式,下一步建议学习TCC模式。理解了AC模式的“自动镜像+全局锁”之后,再看TCC的手动Try-Confirm-Cancel,你会对分布式事务的本质有更深的理解:所有方案都是在“隔离性、一致性、可用性、实现成本”这四者之间做取舍。把AT这套机制搞明白,后面不管是学Seata其他模式还是研究其他分布式事务框架,都能游刃有余。

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

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

立即咨询