Seata中的AT模式
约 2946 字大约 10 分钟
2025-09-05
Seata的AT模式是基于两阶段提交(2PC,Two-Phase Commit)的优化版本,但在传统的2PC的基础上做了重要改进:
- 无需阻塞资源:在第一阶段就直接提交本地事务,释放数据库锁,提高并发性能;
- 通过全局锁+回滚日志来保证事务的最终一致性;
所以AT模式下,它的工作模式的基于Java应用,通过JDBC(代理数据源)访问数据库(支持ACID事务特性)。
AT模式的工作流程
AT模式的工作流程大体上也是分为了两个阶段的:Prepare阶段和Commit/Rollback阶段。
Prepare阶段
Prepare阶段主要负责提交本地事务和undo日志,它主要分为下面几个步骤:
- 解析SQL:Seata的JDBC数据源代理会拦截业务SQL,解析语义,生成前后镜像(before image和after image);
- 执行业务SQL:执行本地事务,但是并不提交,而是分支注册事务到TC(Transaction Coordinator,事务协调器);
- 生成回滚日志:将前后镜像和行锁信息保存到undo_log表中;
- 提交本地事务:提交业务SQL的更改,并释放本地锁(全局锁由TC管理);
- 报告状态:向TC报告分支事务的执行状态(成功/失败);
Commit/Rollback阶段
如果所有分支事务成功:
- TC通知所有的RM提交
- RM收到指令后,异步删除对应的undo日志记录,完成事务;
如果任何一个分支事务失败
- TC通知所有RM回滚
- RM根据undolog中的前后镜像生成反向的SQL执行回滚,并删除undolog;
AT模式中的核心机制
Seata中的事务是一个全局事务,每个全局事务中包含了一个或多个分支事务。在全局事务的执行过程中(全局事务还未提交),某个本地事务提交了,如果Seata没有采取任何的错误,就会导致已提交的本地事务被读取,造成脏读;如果数据在全局事务提交前已提交的本地事务被修改了,则会造成脏写。
在数据库中脏读是指读取到了未提交的数据,但是在Seata中脏读是指读取到了全局事务未提交的数据,全局事务可能包含多个分支事务,某个分支事务提交了不代表全局事务提交了。
脏写
脏写的产生
根据上面的分析,在没有全局锁的情况下会产生脏写的问题。观察下图:
- 事务一和事务都是对account表中的balance字段(假设初始值为300)进行修改;
- 业务一是全局事务,业务二是普通事务;
- 业务一的分支事务一先获取到本地锁,然后执行了本地事务,将数据修改为了200,并且记录了undo日志;
- 业务二后获取到本地锁,然后执行了本地事务,将数据修改为了100;
- 业务一的分支事务二需要更新另外一张表,但是更新的时候出现了异常;
- 业务一的分支事务二反馈事务的执行结果给TC,会触发业务一中全局事务的回滚;
- 业务一的分支事务一进行回滚的时候,发现当前数据库中account字段的值与前镜像和后镜像都不匹配,导致TC无法进行回滚。
因此,在一个全局事务中的多个分支事务执行期间,其他事务也对这个某个分支事务的数据进行修改, 那么在全局事务回滚的时候,就会出现当前数据与某个分支事务的前后镜像都不一致,从而导致TC无法回滚全局事务 —— 脏写就产生了。

脏写的解决策略
在AT模式避免脏写的解决策略就是Seata在TC侧的全局锁来。在需要全局隔离的场景下,加入全局锁的判断逻辑即可避免脏写。
本地锁与全局锁协作逻辑
- 本地锁获取之前不会去争抢全局锁;
- 全局锁获取之前(除非超时)不会释放本地锁;
全局锁才可以保障分布式事务修改中的读、写隔离,这是保障隔离性的关键。Seata的AT模式下,有两种方法来启动全局锁:
- 通过
@GlobalTransaction启动全局锁; - 通过
@GlobalLock启用全局锁;
全局锁的作用逻辑

在加入了全局锁的逻辑后,它有以下几个关键点:
- 本地锁获取之前,不会去争抢全局锁;
- 全局锁获取之前,不会释放本地锁;
- 二阶段回滚时,需要再获取本地锁,在回滚完成之前不会释放全局锁;
但是以上3个步骤会出现死锁,因为业务一的分支事务一要回滚需要先获取本地锁,但是此时本地锁被业务二的分支事务持有了;分支事务二释放本地锁的前提是要获取到全局锁,但是此时全局锁被业务一持有,所以死锁的问题出现了。
AT模式中解决死锁的关键是:为全局锁的重试机制设计超时机制。
@GlobalTransaction和@GlobalLock场景之间的区别在于我们要控制的是两个全局事务,还是一个全局事务和非全局事务。如果是要控制两个全局事务,就使用@GlobalTransaction;如果不是,就使用@GlobalLock。
@GlobalLock不会去注册分支事务,而是在获取本地锁之后简单判断下全局锁是否存在,如果存在就抛出异常,等待其它事务执行完成;反之,就可以放心提交本地事务。
全局锁的生成模式
@GlobalLock不会去注册分支事务,但是仍然会生成前后镜像,这是因为全局锁的key是需要通过前后镜像来生成的。例如,如果是Insert类型的本地SQL,主键通过后镜像获取;如果是Delete类的操作,主键通过前镜像获取;如果是Update类型的操作,理论上前后镜像都可以。
提示
在全局事务下,如果某个分支事务中包含多个SQL语句对数据产生了影响,那么它就会产生多个全局锁;如果是多次修改同一行数据,那么它就还是一个全局锁;
脏读
脏读的产生
对于脏写而言,脏读比较容易理解。其实就是查询到了全局事务还未提交时分支事务提交数据。如果要避免脏读,其实就是保证在全局事务提交前的数据都不可以被读取到。

脏读的解决策略
脏读的策略也是通过全局锁和本地锁来实现的,在查询的时候先获取到本地锁,然后尝试获取全局锁。如果全局锁获取失败,则释放本地锁,再次尝试获取本地锁和全局锁。如果本地锁和全局锁都获取到之后,再去读取数据库中的数据就可以了。

所以在Seata的AT模式是通过对select ... for update语句代理提供全局的读已提交能力,基于select ... for update的原始能力来抢占本地锁,又通过增强的方式假如了查询全局锁以及重试的机制。增强的前提是select ... for update要处于@GlobalTransaction或@GlobalLock的上下文中。
Seata默认的隔离级别
Seata的AT模式默认全局隔离级别是读未提交,如果需要全局读已提交,可以通过select ... for update语句的代理。但是可以看到Seata的AT模式下读已提交的成本很高,传统的数据库的读已提交不需要本地锁,但这里却需要额外添加for update,查询多出了加锁和竞争的开销,另外还要持锁调用TC的lockQuery接口以判断全局锁情况。所以对于全局读已提交的场景非必要下尽量避免使用。
Seata的AT模式的具体实现
AT模式的具体实现和XA模式的具体实现是一致的,它们的区别在于:
# 配置数据源的代理模式为AT(如果XA模式的话配置成XA就可以了)
seata.data-source-proxy-mode=AT其它的跟XA模式的实现都是一致的,通过@GlobalTransaction和@GlobalLock实现全局事务的控制。