大家好,我是十三!欢迎来到十三Tech。

前几年一次生产事故——一个未经 review 的脚本跑了条大事务 UPDATE 没带 WHERE,影响了几百万行。第一反应是 ROLLBACK——还好事务没提交,回滚成功。但回滚花了将近 40 分钟,期间整张表被锁、业务大面积阻塞。后来研究 undo log 才明白——回滚慢不是 bug,是因为 undo log 里要按反向顺序重新构造几百万行的旧值,回写数据页

这件事让我开始研究 undo log——很多人讲它只讲"回滚日志",太浅。undo log 真正的角色是承载 MVCC(多版本并发控制)的"历史版本链"——读操作能"看到"事务开始时的数据快照,全靠 undo log 里保存的旧版本。

这一篇就聊聊 undo log 怎么工作、三种 DML 各自的 undo 形态,以及为什么大事务会让 undo 段涨爆。

undo log:回滚 + MVCC 都靠它

一、undo log 在解决什么——不只是"回滚"

undo log 字面意思是"撤销日志"——记录"修改前的旧值",方便事务回滚或崩溃恢复时还原。这话没错,但只说了一半。

undo log 的两大职责

  1. 事务回滚(原子性 Atomicity):事务执行到一半失败或主动 ROLLBACK,InnoDB 用 undo log 把已做的修改逆向还原
  2. MVCC 的多版本(隔离性 Isolation):其他事务读这行数据时,如果当前版本对自己不可见,就顺着 undo log 链找到可见的旧版本

第二个职责是 undo log 真正重要的地方——没有 undo log 就没有 MVCC,没有 MVCC 就没有高并发读。这也是为什么 undo log 不能像 redo log 那样循环覆盖——只要还有事务可能读到某个旧版本,对应的 undo log 就必须保留。

undo log 是"逻辑日志"——跟 binlog 类似但内容相反。binlog 记"改成什么样",undo log 记"原来什么样"。比如一条 UPDATE users SET age=30 WHERE id=1,binlog 记的是 "age 从 25 变成 30",undo log 记的是 "age 从 30 变回 25"——回滚时反向执行即可。

undo log 三种操作的内部结构

二、三种 DML 的 undo 形态——insert / update / delete

不同操作的 undo 不一样,理解差别能解释很多现象。

INSERT 的 undo:记的是"主键值"。回滚时按主键删除这条记录即可——很轻量。MVCC 不需要 insert 的旧版本(insert 之前根本没这行),所以 insert undo 在事务提交后立即释放

UPDATE 的 undo:记的是"被覆盖前的整行旧值"。回滚时把旧值写回去。MVCC 需要旧版本——update undo 不会立即释放,要等没有任何事务可能读到它时才能清理(purge 线程负责)。

DELETE 的 undo:记的也是"整行旧值"。InnoDB 的 delete 不是真删,是先打"删除标记"(delete mark),后续 purge 线程异步真正回收空间。MVCC 期间,其他事务可能还要读这条"被标记删除但未物理删除"的行——所以 delete undo 也不能立即释放。

关键差别:insert undo 提交即释放;update / delete undo 要等 purge。生产中 undo 段暴涨的根因——大事务里有大量 update / delete,purge 跟不上 undo 增长。

-- 看 undo 大小
SHOW VARIABLES LIKE 'innodb_undo_log_truncate';
SHOW VARIABLES LIKE 'innodb_max_undo_log_size';
SHOW STATUS LIKE 'Innodb_undo_tablespaces%';

三、MVCC 怎么用 undo log——读历史版本

讲完 undo log 本身,再说它在 MVCC 里怎么被使用——这是 undo log 设计的真正动机。

MVCC 的"快照读"流程——当一个事务(事务 A)执行 SELECT

  1. 读取数据页,拿到这行的当前版本
  2. 比对当前版本的"事务 ID"(最近修改这行的事务)跟事务 A 的 ReadView——判断当前版本对 A 是否可见
  3. 不可见就顺着数据页里 roll_ptr(回滚指针)找到 undo log 里的上一个版本
  4. 继续判断这个版本是否可见——不可见就继续顺着 undo 链往下找
  5. 直到找到可见的版本,或者 undo 链到头(说明这行对 A 完全不可见,结果为空)

roll_ptr 是关键——每行数据都有一个 7 字节的 roll_ptr 字段,指向 undo log 里这条行的上一版本。多个历史版本通过 roll_ptr 串成链——这就是"版本链"。

当前版本  ──roll_ptr──→  undo v3  ──roll_ptr──→  undo v2  ──roll_ptr──→  undo v1
[T=100]                 [T=90]                  [T=80]                  [T=70]

事务 A 的 ReadView 决定能看到哪个版本——RC(读已提交)每次 SELECT 重新生成 ReadView,能看到最新提交的;RR(可重复读)事务开始时生成 ReadView,整个事务期间看同一快照。

长事务的坑:长事务持续期间,所有它"看得到"的旧版本 undo log 都不能被 purge——因为 purge 不知道这个长事务哪天才结束。生产中 undo 段涨到几十 GB、磁盘报警,根因几乎都是长事务。长事务是 MySQL 性能的头号杀手,原因之一就在这里。

MVCC 怎么靠 undo log 读历史版本

purge 线程:InnoDB 后台线程,负责清理"不再被任何 ReadView 需要的"undo log。innodb_purge_batch_size 控制一次清理多少。purge 跟不上 undo 增长时,undo 表空间会持续变大——监控 Innodb_undo_tablespaces_totalInnodb_undo_tablespaces_explicit

回头看那个 40 分钟回滚的事故——根因是 undo log 里存了几百万行的旧值,回滚等于"反向执行一遍 update",IO 和 CPU 都得熬过去。真正能避免这种事故的是预防——大事务上线前必须压测、生产中分批执行、关键表加保护性 WHERE。

undo log 真正教会我的,不是"回滚日志",而是用"保留旧版本"换"高并发读"的设计权衡——这个思想不只数据库在用,Git 的版本历史、文件系统的 snapshot、区块链的全节点状态全部包含同一思想。理解 undo log 等于理解了"不可变数据结构"在工程中的价值。

下一篇讲 MVCC——它会用上这一篇的 undo log,把"快照读不加锁"的完整机制讲清楚。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI