【MySQL】深度解析 Redo Log 与灾后恢复
1. 初识 Redo Log 与 WAL
Redo Log 也称重做日志、事务日志,是 InnoDB 独有的日志形式,它的核心作用就是保证事务的持久性。只要事务提交成功,对数据库的修改就永久保存下来了,即使发生系统崩溃,数据也不会丢失。

想要理解 Redo Log 的作用,首先需要明确,数据库在修改数据时,并不是每次操作都直接写入磁盘数据文件。如果每次更新都去磁盘 IO,这个效率是非常低的,因为找到一个数据页进行修改是随机磁盘 IO。
为了解决这个问题,InnoDB 使用了缓冲池(Buffer Pool)。数据页首先被加载到内存中的 Buffer Pool,修改后的数据页(脏页)并不会立即刷回磁盘,而是由后台线程控制其在合适的时机进行刷盘。
但这带来了一个新问题,就是 Buffer Pool 中的数据均未持久化。如果数据库服务器突然断电或崩溃,这些内存数据就会全部丢失。Redo Log 就是为了解决脏页丢失问题而设计的。它的工作原理可以概括为 WAL(Write-Ahead Logging,预写式日志)。
WAL 的原则是:先保证日志的持久化,再保证 Buffer Pool 的持久化。
为什么要这样做?结论很明确,因为日志的持久化快于脏页的持久化。日志的持久化只需将 Redo Log Buffer 中的内容顺序追加到磁盘上的 Redo Log 文件上,这是顺序 IO,相对较快。而 Buffer Pool 中的数据页在磁盘上的物理位置是散乱的,直接刷盘是大量的随机 IO,性能极差。并且 Redo Log 只记录发生了哪些改变,而 Buffer Pool 中是一个一个的完整数据页,从写入粒度角度来看,Redo Log 也是远快于 Buffer Pool 的。
所以,Buffer Pool 并不直接、完整地持久化,而是通过一系列其他机制协作完成持久化。当事务被提交时,InnoDB 会确保将这个事务产生的所有 Redo Log 记录从 Log Buffer 强制刷入(fsync)到磁盘上的 Redo Log 文件中,此时事务就被认为是执行成功的。数据库会在后台选择一个合适的时机(例如:缓冲池满了、系统空闲时),将 Buffer Pool 中的脏页刷新到磁盘的数据文件中。
2. 日志序列号
日志序列号(LSN)是一个单调递增的 64 位整数,可以理解为自数据库实例启动以来,Redo Log 产生的总字节数。它是整个 InnoDB 存储引擎的时间线,通过比较不同实体的 LSN,InnoDB 可以判断数据的一致性状态。
几乎所有数据修改和恢复过程都与 LSN 挂钩:
1. 每个 Redo Log 记录都有一个 LSN。
2. 每个数据页(无论是缓冲池还是磁盘)都记录着最后该页的修改对应的 Redo Log 的 LSN(page_lsn)。也就是说,当页在内存中被修改时,会生成 Redo Log,并更新页的 LSN 为当前 Redo Log 的 LSN。
3. Redo Log 文件上用以控制其循环写入的指针本质就是具体的 LSN。
3. Redo Log 文件组
Redo Log 在磁盘上的文件是以一个日志文件组的形式出现的。使用 innodb_redo_log_capacity 参数来配置文件组的总容量,MySQL 会自动在后台维护多个文件(通常是 32 个)来实现这个总容量。
Redo Log 文件组通过两个指针来进行循环写入的控制,其本质都是具体的 LSN 值:
Write Pos:当前重做日志的写入位置。
Checkpoint:用于标识一个安全点,在这个点之前的 Redo Log 对应的脏页均已经被持久化,因此这些日志就没用了,可以被安全覆盖。
简单区分这两个指针的方法是,日志刷盘会推进 Write Pos,脏页刷盘会推进 Checkpoint。


新日志总是从 Write Pos 开始写入,当其写到最后一个文件的末尾时,会回到第一个文件的开头继续写。
InnoDB 通过很多检查点事件来监听 CheckPoint 这个指针,这些事件的共同目的是,在合适的时机,渐进式无阻塞地完成脏页刷盘,从而平稳推进 Checkpoint。
4. 脏页刷盘时机
脏页刷盘主要通过一系列 InnoDB 的检查点事件在背后驱动,这些事件在逻辑上可以分为下面两类。
4.1 完全检查点
完全检查点(Sharp Checkpoint)会持久化全部脏页,执行完整的清理过程,旨在快速达到一致的状态。这个过程可能会引起 IO 的瞬时峰值,对性能有一定影响。
这类检查点最可能由以下条件触发:
1. 数据库正常关闭时,InnoDB 会持久化全部脏页,这样数据库重新启动时不需要任何恢复。
2. Redo Log 文件即将被写满(Write Pos 马上要追上 Checkpoint),Sharp Checkpoint 会强制刷新一批脏页到磁盘,以迅速推进 Checkpoint,释放可重用的日志文件空间。
4.2 模糊检查点
模糊检查点(Fuzzy Checkpoint)是 InnoDB 在正常运行期间为了避免 Sharp Checkpoint 带来的性能冲击而采用的一系列渐进式刷新策略的总称,它由多个子类型组成,常见的有:
1. 定期检查点
触发条件:由 Master Thread 定时触发。
作用:按照一定的策略(如 LRU 列表或 Flush 列表),小批量地刷新脏页到磁盘。这是一种预防性的刷新,目的是让 Checkpoint 尽量平稳推进,避免积压过多脏页,从而降低触发 Sharp Checkpoint 的概率。
2. LRU 检查点
触发条件:当 Buffer Pool 满了,需要从缓冲池的 LRU 列表尾部(Old 区尾部)淘汰一些页以容纳新读入的页时。
作用:进行淘汰的过程中,如果该页是脏页,则必须先将它刷新到磁盘。这本质上也推进了 Checkpoint。
3. 异步刷新检查点
触发条件:当 Redo Log 空间使用率超过一定比例时,由 Page Cleaner 线程在后台异步地批量刷新脏页。这是对 Sharp Checkpoint 的优化,旨在更早、更从容地刷新脏页,避免系统陷入日志空间即将用尽的紧急状态。
作用:核心的 Fuzzy Checkpoint 机制,用于主动管理 Redo Log 空间,确保系统始终有可用的日志空间。
4. 脏页过多检查点
触发条件:当缓冲池中脏页的超过一定比例时。
作用:这个检查点会主动限制脏页的数量,保持系统的健康状态。
5. 日志刷盘时机
5.1 事务提交时
这是最核心的机制,也是设计 Redo Log 的初衷。通过 innodb_flush_log_at_trx_commit 参数来精确控制这一行为:
innodb_flush_log_at_trx_commit = 1
最安全的行为,默认行为。事务提交时,Log Buffer 写入 Redo Log File 后立即执行 fsync 系统调用,强制将操作系统缓存(page cache)刷到物理磁盘。
能够最大程度降低任何级别的故障带来的影响,但性能略低,因为每次提交都是一次磁盘 IO。
innodb_flush_log_at_trx_commit = 0
风险最高的行为,相当于关闭事务提交的触发机制,只依靠后台线程每秒一次的刷盘。这表示故障时可能丢失最近 1 秒钟内提交的所有事务。
innodb_flush_log_at_trx_commit = 2
折中处理。在事务提交时,将 Log Buffer 的内容写入操作系统的文件系统缓存,但不执行 fsync 系统调用刷盘。
如果 MySQL 进程崩溃,事务不会丢失。但如果出现操作系统级别的崩溃或服务器断电,则事务可能丢失,因为数据可能尚在操作系统缓存中,未落盘。
5.2 定期刷盘
InnoDB 的后台主线程会每秒一次地执行一次刷盘操作,这个行为总会发生。它确保了即使没有事务提交,那些在 Log Buffer 中停留了一段时间的 Redo Log 也会被持久化,防止数据丢失的窗口过大。
5.3 其他因素
1. 当 Log Buffer 的已使用空间达到或超过其总大小的大约一半时,InnoDB 会主动将其中的内容写入 Redo Log File。
2. 在推进 Checkpoint 的过程中,根据 WAL 的原则,InnoDB 必须保证日志刷盘早于对应的脏页刷盘,因此这可能会间接地促使 Redo Log 被持久化,但这一般不是主要原因。
3. 正常关闭数据库服务器时,InnoDB 会执行一个完整的清理过程,其中就包括将 Log Buffer 中的所有剩余内容刷到 Redo Log File,以确保所有数据在关闭前都处于一致的状态。
4. 为了保持 Binlog 和 Redo Log 在复制环境中的一致性,在将 Binlog 刷盘的同时,也会触发 Redo Log 的刷盘。
6. 双写缓冲区
到此为止,文章已经总结了 MySQL 灾备所依赖的大部分重要组件,还差一个,就是双写缓冲区(Doublewrite Buffer)。这个组件的作用是什么?它的出现自然也是源于实际灾备过程中的问题,并且是 Redo Log 解决不了的问题。
我们设想一个这样的场景,当一个脏页正在被持久化的过程中发生了系统断电等操作系统级的故障,这会导致磁盘上的数据页是不完整的、损坏的,这被称为部分页写入。
这是可能发生的,因为一个 InnoDB 数据页的大小一般为 16 KB,而操作系统进行 IO 读写的基本单位是 4 KB 的块,这个数据页可能只有部分块被写入磁盘。
为什么 Redo Log 无能为力?Redo Log 只是记录了最近一段时间这个页上发生的变化,但现在这个页已经不完整了,Redo Log 根本无从下手。
双写缓冲区可以在一定程度上解决这个问题:当 InnoDB 想要将一堆脏页持久化时,它不直接把这些脏页写回具体其对应的数据文件位置,而是先将其顺序地写入双写缓冲区在磁盘上的一块预设的连续区域。
这个思想其实和 “先刷日志,后刷脏页” 的思想是完全一样的,都是以写放大为代价来换取顺序 IO,为随机 IO 做安全垫。持久化脏页是段较长的时间窗口,所以你会发现这一切种种机制本质上都是在想办法缩短这个时间窗口,从而尽量规避数据丢失的风险。
这个思想其实很好理解,举个生活中的例子:假设现在你坐在教室里考试,但是你不太会做,只能靠手机作弊。你的好哥们将答案发给了你,那么你是会将这些答案先迅速地在草稿纸上一块连续的空白处抄一遍,还是直接一道题一道题地把答案填到相应位置?如果你求稳,很可能会选择后者,毕竟监考老师一直在溜达,手机随时有可能不能再看了...
数据库就是典型的求稳场景,虽然 Doublewrite Buffer 引入了额外的磁盘写入,但比起数据安全性,这是个可以接受的代价。
回到 Doublewrite Buffer 上来,可能有人会有疑问,一般缓冲区指的是内存中的区域,但是刚刚好像说双写缓冲区在磁盘上?其实不是的,InnoDB 的确在内存中维护了一个 Doublewrite Buffer,当脏页需要刷新时,首先被复制到这块内存,作为页写入磁盘前的暂存区。
但更关键的部分是,Doublewrite Buffer 在磁盘上对应一块固定的存储位置。在服务器数据目录的 .dblwr 文件中,这样的文件有两个,每个可以容纳 64 个页(64 × 16KB = 1MB),专门用于存储页的临时副本。这是它真正起作用的地方,因此有时会用双写缓冲区直接指代这块磁盘空间。
还有一个问题,如果在写入双写缓冲区的过程中,出现操作系统级崩溃,导致其中的页残缺怎么办?其实好办,我们直接丢弃残缺页就可以了,因为它只是副本,磁盘数据文件中依然有该页的完整数据,之后再使用 Redo Log 恢复该页就可以了。这也体现出 InnoDB 设计的精妙之处,双写的意义不仅是先快写再慢写,它还保证了这两处数据同时只有一处在被写,如果其中一处出了问题,另一处永远可以用来灾救。
7. 灾后恢复
当 MySQL 实例异常关闭(如断电、kill -9)后再次启动时,InnoDB 存储引擎会自动触发崩溃恢复流程。这个过程可以概括为以下三个阶段。
7.1 确定恢复范围
在前文中反复提到一个叫检查点的概念,这个概念最大的用武之地就在这里。我们已经知道,Checkpoint 前面的(LSN 更小的)所有日志对应的脏页都已持久化,这就表示,数据库恢复时,只需要从检查点开始应用 Redo Log,而不需要从最旧的一条日志开始。这大大降低了数据库恢复的成本。
因此恢复的起点是 Checkpoint 对应的 LSN,终点是 Redo Log 中记录的最后一个有效的 LSN。在这个过程中,InnoDB 还会构建一个哈希表,用于记录所有需要回滚的事务。
7.2 Redo 阶段
这是恢复的最核心阶段,也是 Redo Log 发挥其作用的阶段。
InnoDB 从刚刚确定的恢复起点开始,按 LSN 顺序依次读取 Redo Log 记录。
我们已经知道 Redo Log 是一种物理日志,也就是说它直接记录的就是命令在物理磁盘上的修改,比如:“在表空间 A、页号 B、偏移量 C 的位置,将数据 0x1234 改为 0x5678”。在崩溃恢复时,它不需要解析 SQL 语句,也不需要走执行流程,这种设计旨在提高恢复速度。因为数据库崩溃是异常行为,必须尽快让服务可用。
在重放每条 Redo Log 时,InnoDB 首先根据日志中的表空间和页号,将对应的数据页从磁盘加载到内存(Buffer Pool)中。
拿到数据页后,比较其 LSN 是否小于该条 Redo Log 的 LSN,当前仅当小于时对其应用 Redo Log 并更新 LSN。这是因为,Page LSN 反映的是该页最近的一次改变对应的时间点,如果这个时间点大于或等于 Redo Log 的时间点,那说明这条 Redo Log 一定已经被应用过了,不可以再次应用,这严格保证了幂等性。
这个时候,数据库在 Buffer Pool 中的状态就与崩溃发生瞬间的状态完全一致了。但这个状态不仅包含了所有已提交事务的修改,也包含了所有未提交事务的修改。因为 Redo Log Buffer 的持久化受多重因素控制,不仅是事务提交,当前数据库的状态很可能执行了一些不完整的事务。
而事务必须要确保其原子性,要么全部执行成功,要么全部都不执行。因此我们必须将未提交事务的修改回滚掉,这就要进入恢复的第三个阶段。
7.3 Undo 阶段
对于每个未提交的事务,InnoDB 会进入其 Undo Log 区域,然后反向应用这些 Undo Log 记录,将未提交事务所做的修改全部撤销。这本质上就是执行了一次事务回滚操作。
关于 Undo Log 的回滚和相关知识点还比较复杂,这里因为和 Redo Log 关系不大,只是简述。事务和 Undo Log 的相关内容将在另一篇文章中总结。
数据库现在处于一个持久且一致的状态,只包含了崩溃前所有已提交事务的修改。
