返回首页

19|redo 与 undo 日志

适用版本:MySQL 8.0.x

学 InnoDB 时,redo log 和 undo log 是绕不过去的两块核心内容。很多同学第一次接触时会把它们记成两句口诀:

  • redo:用于“重做”
  • undo:用于“回滚”

这个记忆并没有错,但远远不够。真正进入工程实践后,你会发现:

  • 为什么 MySQL 崩溃后能恢复?
  • 为什么事务提交了但数据页还没刷盘也不怕?
  • 为什么快照读能读到旧版本?
  • 为什么长事务会拖垮 purge,导致历史版本堆积?

这些问题都和 redo、undo 直接相关。

这篇文章会从原理、执行过程、典型排障点三个角度,把 redo 与 undo 日志讲清楚。


一、先给出一句总定义

可以先记住下面这组关系:

  • redo log:记录“数据页做过什么修改”,用于崩溃恢复持久性保证
  • undo log:记录“修改前是什么样子”,用于事务回滚MVCC

两者看起来都和“旧数据、新数据”有关,但职责完全不同:

  • redo 面向的是“提交后的恢复
  • undo 面向的是“修改前的可撤销与可见性

理解这层分工后,很多事务行为就容易串起来了。


二、为什么需要 redo log

1. 直接刷数据页为什么不行

假设你执行一条更新语句:

UPDATE account SET balance = balance - 100 WHERE id = 1;

如果每次事务提交都立刻把被修改的数据页刷到磁盘,会遇到几个问题:

  1. 随机 I/O 成本太高:数据页分布在不同位置,直接刷页很慢。
  2. 一条事务可能修改多个页:全部同步落盘,延迟很高。
  3. 崩溃时写到一半怎么办:只写完部分页会造成状态不一致。

所以 InnoDB 采用的是更高效的方案:

  • 先修改内存中的页
  • 同时把这次修改对应的 redo 记录下来
  • 提交时优先保证 redo 持久化
  • 数据页以后再择机刷盘

这就是 WAL(Write-Ahead Logging)

2. WAL 的核心价值

WAL 的关键思想是:

只要变更日志先安全落盘,数据页之后再刷也没关系。

这样做的收益非常明显:

  • 顺序写日志通常比随机写数据页更快
  • 提交延迟更低
  • 宕机后可以靠 redo 把已提交但未刷盘的数据补回来

所以 redo log 本质上是 InnoDB 事务持久性的基础设施。


三、redo log 记录的到底是什么

很多初学者会误以为 redo log 记录的是一条完整 SQL。实际上不是。

1. redo 不是 SQL 文本

redo log 记录的是 页级物理修改的描述,可以简单理解为:

  • 哪个页被改了
  • 改了页中的哪个位置
  • 改成了什么样

它更接近“存储引擎内部修改记录”,而不是“用户提交的业务语句”。

这也是为什么:

  • redo 由 InnoDB 引擎层维护
  • binlog 由 Server 层维护

两者服务的目标不同。

2. redo 的组织方式

学习上你可以先把 redo log 理解成:

  • 有一块 redo log buffer 在内存中
  • 后台把它刷到磁盘上的 redo 文件中
  • 每次修改页时,都会生成对应的 redo 记录

这些记录按顺序写入,并通过 LSN(Log Sequence Number) 表示日志推进位置。

LSN 可以理解成 redo 系统中的“进度条”。


四、redo log 如何保证事务提交不丢失

我们用一条更新语句来走一遍大致流程。

1. 执行更新时

当事务执行更新:

BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;

InnoDB 大致会做这些事:

  1. 把目标数据页读入 Buffer Pool(如果不在内存)
  2. 修改内存页
  3. 生成对应的 redo 记录,写入 redo log buffer
  4. 生成 undo 记录,留作回滚和 MVCC 使用

2. 提交时

事务执行 COMMIT 时,不一定要求数据页已经刷盘,但通常要求:

  • 与该事务相关的 redo 已经按照刷盘策略处理

最典型的强一致配置是:

  • innodb_flush_log_at_trx_commit = 1

这表示每次提交都把 redo 刷到磁盘文件并执行 fsync。这样即使数据库进程或机器宕机,也能依赖 redo 做恢复。

3. 崩溃恢复时

如果事务已经提交,但脏页还未来得及落盘,实例崩溃重启后:

  • InnoDB 会扫描 checkpoint 之后的 redo
  • 把尚未反映到数据页上的修改重新应用

这就是“重做”的含义。


五、checkpoint 在 redo 体系中的作用

如果 redo 一直无限追加,那磁盘空间会持续增长,恢复成本也会越来越高。

所以 InnoDB 需要 checkpoint 机制。

1. checkpoint 是什么

可以把 checkpoint 理解为:

告诉系统,某个 LSN 之前的修改,已经安全体现在数据页中了。

有了这个位置后:

  • 崩溃恢复不必从很久以前的日志开始
  • redo 空间可以循环复用

2. 为什么刷脏页不只是为了“落盘”

很多人以为后台刷脏页只是为了减少宕机损失。实际上还有两个重要原因:

  1. 推进 checkpoint
  2. 释放 redo 可用空间

如果刷脏页跟不上,redo 空间压力变大,前台事务也可能受到影响。

这就是为什么写入压力很大时,数据库性能问题常常不只是“磁盘忙”,还会表现为 checkpoint age 紧张、脏页比例高、提交抖动。


六、为什么还需要 undo log

只靠 redo 并不能完成事务系统。

因为 redo 回答的是:“提交后如何恢复”;但数据库还要回答另外两个问题:

  1. 如果事务还没提交,需要撤销怎么办?
  2. 如果另一个事务想看到一致性快照,旧版本从哪里来?

这时就需要 undo log。

1. undo 的基本作用

undo log 记录修改前的旧版本信息,主要服务两类场景:

  • 事务回滚:把数据恢复成修改前状态
  • 一致性读:提供历史版本给快照读使用

2. 一个直观例子

假设原始数据是:

id=1, balance=1000

事务 T1 执行:

UPDATE account SET balance = 900 WHERE id = 1;

这时 InnoDB 会:

  • 在数据页中准备写入新值 900
  • 在 undo log 中保留旧值相关信息 1000

如果 T1 回滚,就能按 undo 把记录恢复回去。

如果另一个事务 T2 需要看到旧快照,也可能沿着版本链读到旧值。


七、undo log 与 MVCC 的关系

很多人学 MVCC 时会背诵“通过 Read View 和 undo log 实现”,但没有真正建立图景。

1. 行记录不是只有一个版本

在 InnoDB 中,一条记录被更新时,并不是简单地“把旧值覆盖掉然后旧值消失”。

更准确地说:

  • 当前记录保存最新版本
  • 旧版本通过 undo 链接起来
  • 一致性读可以沿着版本链查找“对当前事务可见的那个版本”

2. 快照读为什么不加锁也能读

因为快照读读的是历史可见版本,而不是非要读取“当前最新值”。

例如:

  • T1 更新但未提交
  • T2 进行普通 SELECT

在可重复读隔离级别下,T2 往往会依据自己的 Read View,结合 undo 版本链,读到一个对自己可见的历史版本,而不是被 T1 阻塞。

3. 这也是长事务危险的原因之一

如果一个长事务一直不结束,它持有的 Read View 会让很多旧版本不能及时清理。

结果就是:

  • undo 历史版本堆积
  • purge 跟不上
  • 表空间膨胀
  • 查询链路变长
  • 整体性能下降

所以工程上要非常警惕“长时间不提交的事务”。


八、undo log 在回滚时怎么工作

1. 回滚不是“假装没发生”

事务执行失败或手动 ROLLBACK 时,InnoDB 会根据 undo log 做反向操作,把被修改的数据恢复到事务开始前或保存点前的状态。

例如:

BEGIN;
UPDATE product SET stock = stock - 1 WHERE id = 10;
ROLLBACK;

最终数据会回到修改前的值。

2. 回滚也可能很重

回滚不是零成本的。特别是在这些场景下:

  • 大事务更新了很多行
  • 执行了大批量删除
  • 程序异常导致事务超大

这时回滚可能持续很久,甚至造成更多锁等待和系统抖动。

工程上一个很实用的经验是:

大批量改数据时,宁可分批提交,也不要把几百万行修改塞进一个超大事务里。


九、redo 与 undo 在一次更新里如何配合

下面把二者放到同一个事务流程中看。

1. 修改发生时

当一行被更新时:

  • InnoDB 先生成 undo,保留旧版本线索
  • 再修改内存中的数据页
  • 同时生成 redo,记录这次页变更

2. 提交时

提交成功后:

  • redo 负责保证“这次修改之后能恢复出来”
  • undo 不会立刻消失,因为 MVCC 还可能需要旧版本

3. 后续清理

当系统确认某些旧版本已不再被任何活跃事务需要时,后台 purge 线程才会逐步清理相关 undo 历史。

所以:

  • redo 更像“面向未来恢复”
  • undo 更像“保留过去版本”

一个负责让已提交事务不丢,一个负责让未提交事务可撤销、已提交事务的旧版本可见。


十、redo 与 undo 的常见混淆点

1. redo 不是回滚日志

redo 的目标不是把修改撤销,而是把已提交修改补做回来。

2. undo 不是持久化提交保证

事务提交后的持久性,核心依赖 redo,而不是 undo。

3. binlog 也不是 redo

binlog 记录的是逻辑层面的变更事件,主要用于复制和恢复;redo 是引擎层的物理日志,主要用于崩溃恢复。

这三者容易混,但一定要分清。


十一、工程实践中最重要的几个参数

1. innodb_flush_log_at_trx_commit

这个参数决定 redo 在提交时的刷盘策略,是最常被问到的参数之一。

常见取值:

  • 1:每次提交都写文件并 fsync,持久性最好
  • 2:每次提交写文件,但 fsync 由后台周期执行
  • 0:提交时只写 redo buffer,由后台周期写文件并刷盘

工程上的一般建议:

  • 金融、订单、核心账务:通常用 1
  • 允许极端情况下少量秒级数据丢失的非核心场景:才可能评估 2

不要为了短期 TPS 好看而轻易牺牲持久性。

2. redo 容量相关配置

在 MySQL 8.0 中,redo 空间容量可以通过相关参数统一管理。写入压力较高时,如果 redo 空间过小,容易导致 checkpoint 压力增大、刷盘更激进。

观察点包括:

  • 写入高峰是否出现突刺抖动
  • 脏页刷盘是否过于频繁
  • 恢复时间目标是否可接受

3. undo / purge 相关观测

虽然业务开发通常不直接配置 undo 结构细节,但要学会关注:

  • 是否存在长事务
  • 历史列表长度是否持续升高
  • purge 是否跟不上

这些都与 undo 历史清理直接相关。


十二、几个非常实用的排查视角

1. 为什么数据库提交突然变慢

可以优先怀疑:

  • redo 刷盘压力变大
  • 磁盘 fsync 延迟上升
  • checkpoint 推进困难
  • 大量脏页导致后台刷盘过猛

2. 为什么表空间越来越大

除了业务数据本身增长,还要考虑:

  • 大量删除后空间未回收
  • 长事务导致 undo 相关历史版本迟迟不能清理
  • purge 落后

3. 为什么普通查询越来越慢

如果不是索引问题,也可能和 MVCC 历史链变长有关。

典型诱因:

  • 长事务长期持有快照
  • 高频更新表积累大量旧版本

4. 为什么回滚会持续很久

因为回滚本身也要做工作。超大事务的回滚不仅慢,而且常常会对生产环境造成二次冲击。


十三、常见命令与观测方法

1. 查看 InnoDB 运行状态

SHOW ENGINE INNODB STATUS\G

重点关注:

  • 事务列表
  • 历史列表长度
  • 日志相关信息
  • 最近死锁
  • Buffer Pool / I/O 状态

2. 查看长事务

SELECT *
FROM information_schema.innodb_trx;

这个视图常用于定位:

  • 长时间未提交事务
  • 锁等待链路
  • 谁在拖慢 purge

3. 查看参数

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'innodb_redo%';

调优之前,先确认当前配置,不要凭经验猜。


十四、一个完整的脑图式理解

可以用下面这组关系来串联:

  1. 事务修改数据时:生成 undo、修改内存页、记录 redo
  2. 事务提交时:优先保证 redo 持久化
  3. 事务回滚时:依据 undo 撤销修改
  4. 一致性读时:依据 Read View + undo 版本链读取历史版本
  5. 实例崩溃后:依据 redo 重做已提交但未刷盘的修改
  6. 后台清理时:purge 逐步删除不再需要的 undo 历史版本

这才是 redo 与 undo 在 InnoDB 中的完整位置。


十五、开发与运维的实战建议

1. 避免长事务

这是最重要的一条。长事务会带来:

  • undo 累积
  • purge 落后
  • 锁持有时间变长
  • 主从延迟放大
  • 崩溃恢复成本上升

2. 大批量修改要分批提交

不要一次更新几百万行还放在一个事务里。推荐做法:

  • 按主键范围分页
  • 每批固定数量
  • 每批独立提交
  • 配套限速和监控

3. 核心业务优先保证 redo 持久性

核心库不要轻易把 innodb_flush_log_at_trx_commit 改成低持久性模式。

4. 关注磁盘延迟而不只看 CPU

很多“数据库突然慢”的根因其实在日志刷盘路径,而不是 SQL 本身。日志设备的稳定性非常关键。

5. 回滚成本要纳入变更方案

做批量变更前,不只要问“执行多久”,还要问:

  • 如果中途停止,回滚多久?
  • 会不会阻塞线上请求?
  • 是否有更安全的分批方案?

十六、小结

redo 与 undo 是 InnoDB 事务系统的两条主线:

  • redo 解决的是“提交后怎么保证不丢、宕机后怎么恢复”
  • undo 解决的是“未提交怎么回滚、已提交旧版本怎么被快照读看到”

如果只记口诀,你会觉得它们抽象;但一旦把它们放进“更新、提交、回滚、快照读、崩溃恢复”这个完整流程里,就会发现:

它们其实是在回答数据库系统最基本的几个问题——

  • 数据怎么安全提交?
  • 出故障后怎么恢复?
  • 并发下怎么既读得快又读得对?

理解到这里,后面再学 MVCC、binlog、两阶段提交与复制机制,就会自然很多。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

18 | InnoDB 存储引擎入门

下一篇

20|binlog 与两阶段提交