
在使用 Seata 的 AT 模式时,观察数据库,会发现了一张名为 undo_log 的表。这张表的存在,往往会让人产生一个误解:Seata 是否直接利用了数据库(如 MySQL InnoDB)底层的 undo log 机制来实现回滚?
如果不能清晰区分这两者的边界,就无法真正理解分布式事务中"两阶段提交"在工程落地时的妥协与设计哲学。
今天我们就从源码和设计原理的角度,彻底拆解 Seata 的 undo log 机制。
Seata AT 模式:应用层回滚 vs 引擎层回滚
直接抛出结论:Seata 的 undo_log 完全是应用层自己生成的"逻辑日志",它与 MySQL 底层的 undo log 没有任何物理上的依赖关系。
虽然名字都叫 "undo",但它们的维度完全不同:
- MySQL undo log :是引擎层的物理/逻辑日志,用于 MVCC 快照读和未提交事务的原子性回滚。一旦事务 Commit,这部分日志在回滚维度就失效了。
- Seata undo_log :是业务层的数据快照(Image),用于分布式事务第二阶段的"补偿回滚"。即便本地事务已经 Commit,它依然有效。
一、 数据镜像 (Data Image) 生成机制
Seata AT 模式的核心魔法在于 JDBC 数据源代理 (DataSource Proxy)。它会在你的业务 SQL 执行前后,"偷偷"加塞了几条 SQL,完成了数据快照的记录。
假设你的业务 SQL 如下:
sql
UPDATE product SET stock = 90 WHERE id = 1;
-- 假设执行前 stock = 100
Seata 在执行这条 SQL 时,会将其拆解为一个包含 4 个步骤的内部流程。这个过程完全由 Seata 客户端(RM)在内存中完成:
1. 一阶段:生成快照 (Phase 1)
Step 1:前置镜像查询 (Before Image)
在执行业务 SQL 之前,Seata 会解析你的 SQL 语句,自动构造一条查询语句,把"更新前"的数据查出来:
sql
SELECT id, stock FROM product WHERE id = 1 FOR UPDATE;
- 结果 :
id=1, stock=100。 - 动作 :将结果暂存内存,标记为 Before Image。
Step 2:执行业务 SQL
执行你写的 UPDATE 语句。此时数据库中的数据已经变成了 90。
Step 3:后置镜像查询 (After Image)
业务 SQL 执行完,但本地事务提交之前,Seata 再次查询:
sql
SELECT id, stock FROM product WHERE id = 1;
- 结果 :
id=1, stock=90。 - 动作 :将结果暂存内存,标记为 After Image。
Step 4:插入回滚日志
Seata 将 Before Image 和 After Image 打包成一个 JSON/Blob 格式的对象,作为一条记录插入到业务库的 undo_log 表中。
sql
INSERT INTO undo_log (branch_id, xid, rollback_info, ...)
VALUES (..., '{"beforeImage":{100}, "afterImage":{90}}', ...);
Step 5:本地事务提交
注意,此时 MySQL 的本地事务已经 Commit 了! 锁已经被释放,MySQL 认为这笔交易结束了。
下面是这个过程的时序图:
代码段
MySQL Database Seata JDBC Proxy 业务代码 MySQL Database Seata JDBC Proxy 业务代码 开启本地事务 执行 UPDATE product SET stock=90... SELECT ... FOR UPDATE (Before Image) stock = 100 执行业务 UPDATE SELECT ... (After Image) stock = 90 INSERT INTO undo_log ... COMMIT (提交本地事务) 执行成功
二、 回滚机制:反向补偿 (Compensation)
明白了生成机制,回滚机制就很容易理解了。
既然本地事务已经 Commit,MySQL 原生的 undo log 就无法使用了(因为 MySQL 不支持回滚已提交的事务)。当 TC(事务协调器)决议全局回滚时,Seata 执行的是应用层的补偿操作。
二阶段回滚 (Phase 2 Rollback) 流程:
-
查找日志 :RM 根据 XID 和 Branch ID 去
undo_log表里找到对应的记录。 -
数据校验 :校验
After Image与当前数据库的数据是否一致(为了防止脏写,如果中间有人改过数据,回滚会失败并报警)。 -
生成反向 SQL :根据
Before Image(stock=100),自动生成还原数据的 SQL:sqlUPDATE product SET stock = 100 WHERE id = 1; -
执行还原:执行这条反向 SQL,将数据改回原样。
-
清理日志 :删除
undo_log中的记录。
三、 深度对比:Seata undo_log vs MySQL undo log
为了彻底厘清区别,我整理了如下对比表:
| 特性 | Seata undo_log | MySQL undo log |
|---|---|---|
| 角色 | 分布式事务的"后悔药" | 本地事务的原子性保证 |
| 生成者 | Java 应用层 (Seata Client) | 数据库引擎层 (InnoDB) |
| 存储位置 | 业务库中的一张普通表 (undo_log) |
系统表空间或专门的 Undo Tablespace |
| 内容格式 | 逻辑数据 (JSON),包含前后镜像 | 物理/逻辑混合日志,二进制格式 |
| 生命周期 | 事务提交后依然存在 (直到 Phase 2 完成) | 事务提交后即失去回滚作用 (仅用于 MVCC) |
| 回滚原理 | 执行 反向 SQL (补偿) | 从日志中恢复旧版本页 |
四、 架构思考
为什么 Seata 不直接利用数据库的 XA 协议或 undo log,而要自己搞一套 undo_log 表?
这是为了解决 长事务带来的锁竞争问题。
- 传统 XA 模式:在全局事务未完成前,必须一直持有数据库的锁。如果网络波动导致全局事务耗时 2 秒,那这行数据就被锁了 2 秒,并发性能极差。
- Seata AT 模式 :通过一阶段本地提交 ,快速释放了数据库层面的锁。这让数据库的并发能力不再受限于分布式事务的耗时。而
undo_log表,就是为了换取这种高性能而付出的存储成本。
Seata 的 "AT" 全称是 Automatic Transaction,它的本质就是:Seata 帮你把 TCC (Try-Confirm-Cancel) 模式中的 Cancel 阶段代码给自动生成了。
理解了这一点,你也就理解了 Seata AT 模式的设计精髓。