天有不测风云,数据库有旦夕祸福。
前面写 Redo 日志的文章介绍过,数据库正常运行时,Redo 日志就是个累赘。
现在,终于到了 Redo 日志扬眉吐气,大显身手的时候了。
本文我们一起来看看,MySQL 在崩溃恢复过程中都干了哪些事情,Redo 日志又是怎么大显身手的。
本文介绍的崩溃恢复过程,包含 server 层和 InnoDB,不涉及其它存储引擎,内容基于 MySQL 8.0.29 源码。
MySQL 崩溃也是一次关闭过程,只是比正常关闭着急了一些。
正常关闭时,MySQL 会做一系列收尾工作,例如:清理 undo 日志、合并 change buffer 缓冲区等操作。
具体会进行哪些收尾工作,取决于系统变量 innodb_fast_shutdown 的配置。
崩溃直接就是戛然而止,撂挑子不干了,还没来得及进行的那些收尾工作怎么办?
那就只能等待下次启动的时候再干了,这就是本文要介绍的崩溃恢复过程。
MySQL 一旦崩溃,Redo 日志就要去拯救世界了(MySQL 就是它的世界),Redo 日志拯救世界的方式就是把还没来得及刷盘的脏页恢复到崩溃之前那一刻的状态。
虽然 Redo 日志能够用来恢复数据页,但这是有前提条件的:数据页必须完好无损的状态。
本文我们把系统表空间、独立表空间、undo 表空间中的页统称为数据页。
如果数据页刚写了一半,MySQL 就戛然而止,这个数据页就损坏了,面对这种情况,Redo 日志也是巧妇难为无米之炊。
Redo 日志拯救世界之路就要因为这个问题停滞不前吗?
那显示是不能的,这就该轮到两次写上场了。
两次写的官方名字是 double write,它包含内存缓冲区和 dblwr 文件两个部分,InnoDB 脏页刷盘前,都会先把脏页写入内存缓冲区,再写入 dblwr 文件,成功之后才会把网页刷盘。
两次写通过系统变量 innodb_doublewrite 控制开启或关闭,本文内容基于该系统变量的默认值 ON,表示开启两次写。
如果网页写入内存缓冲区和 dblwr 文件的程中,MySQL 崩溃了,表空间中对应的数据页还是完整的,下次启动时,不需要用两次写页面修复这个数据页。
如果脏页刷盘时,MySQL 崩溃了,表空间对应的数据页损坏了,下次启动时,应用 Redo 日志到数据页之前,需要用两次写页面修复这个数据页。
dblwr 文件 默认位于 MySQL 数据目录下:
[csch@csch /usr/local/mysql_8_0_29/data] ls -l | grep dblwr
-rw-r----- 1 csch staff 192K 8 27 12:04 #ib_16384_0.dblwr
-rw-r----- 1 csch staff 8.2M 8 1 16:29 #ib_16384_1.dblwr
MySQL 启动过程中,会把 *.dblwr 文件中的所有两次写页面加载到两次写内存缓冲区,并用内存缓冲区中的两次写页面修复损坏的数据页,然后再应用 Redo 日志到数据页。
应用 Redo 日志到数据页(3.4 小节),需要先读取 Redo 日志(3.3 小节)。
读取日志 Redo 日志,需要有个起点,起点就是最后一次 checkpoint 的 lsn(3.1 小节)。
应用 Redo 日志有一个前提:数据页必须是完好无损的。要保证数据页的完整性,应用 Redo 日志之前需要修复损坏的数据页(3.2 小节)。
修复损坏数据页只需要保证在应用 Redo 日志之前就行了,之所以安排在 3.2 小节,是遵循了源码中的顺序。
了解本节安排内容顺序的逻辑,有助于理解应用 Redo 日志恢复数据页的过程,接下来我们正式进入下一个环节。
读取 Redo 日志之前,必须先确定一个起点,这个起点就是 InnoDB 最后一次 checkpoint 操作的 lsn,也就是 last_checkpoint_lsn。
每个 Redo 日志文件的前 4 个 block 都是保留空间,不会用来写 Redo 日志,last_checkpoint_lsn 和其它 checkpoint 信息一起,位于第 1 个 Redo 日志文件的第 2、4 个 block 中。
Redo 日志文件中每个 block 的大小为 512 字节。
InnoDB 每次进行 checkpoint 操作时,都会把 checkpoint_no 加 1,用于标识一次 checkpoint 操作。
然后把本次 checkpoint 信息写入 Redo 日志文件的第 2 或第 4 个 block 中。具体写入哪个 block,取决于 checkpoint_no。
如果 checkpoint_no 是奇数,checkpoint 信息写入第 4 个 block。
如果 checkpoint_no 是偶数,checkpoint 信息写入第 2 个 block。
确定读取 Redo 日志的起点时,从第 2、4 个 block 中读取较大的那个 last_checkpoint_lsn 作为起点。
为什么 checkpoint 信息要存储到 2 个 block 中?
这是一个用于保证 checkpoint 信息安全性的简单好用的方法,因为每次 checkpoint 只会往其中一个 block 写入信息。
万一就在某次写 checkpoint 信息的过程中 MySQL 崩溃了,有可能导致正在写入的这个 block 中的 checkpoint 信息不正确。
这种情况下,另一个 block 中的 checkpoint 信息肯定是正确的了,因为它里面的信息是上一次正常写入的。
能够用这种冗余方式来保证 checkpoint block 的安全性,基于一个前提:last_checkpoint_lsn 不需要那么精确。
last_checkpoint_lsn 比实际需要应用 Redo 日志起点处的 lsn 小是没关系的,不会造成数据页不正确,只是会多扫描一点 Redo 日志而已,应用 Redo 日志时会过滤已经刷盘的脏页对应的 Redo 日志。
把两次写文件中的所有数据页都加载到内存缓冲区之后,需要用这些页来把系统表空间、独立表空间、undo 表空间中损坏的数据页恢复到正常状态。
正常状态指的是 MySQL 崩溃之前,数据页最后一次正确的刷新到磁盘的状态。
恢复数据页的过程是对两次写内存缓冲区中的所有数据页进行循环,从两次写数据页中读取表空间 ID、页号,然后根据表空间 ID 和页号去系统表空间、独立表空间、undo 表