MySQL 崩溃恢复过程分析-元一软件 天有不测风云数据库有旦夕祸福。前面写 Redo 日志的文章介绍过数据库正常运行时Redo 日志就是个累赘。现在终于到了 Redo 日志扬眉吐气大显身手的时候了。本文我们一起来看看MySQL 在崩溃恢复过程中都干了哪些事情Redo 日志又是怎么大显身手的。本文介绍的崩溃恢复过程包含 server 层和 InnoDB不涉及其它存储引擎内容基于 MySQL 8.0.29 源码。1. 概述MySQL 崩溃也是一次关闭过程只是比正常关闭着急了一些。正常关闭时MySQL 会做一系列收尾工作例如清理 undo 日志、合并 change buffer 缓冲区等操作。具体会进行哪些收尾工作取决于系统变量 innodb_fast_shutdown 的配置。崩溃直接就是戛然而止撂挑子不干了还没来得及进行的那些收尾工作怎么办那就只能等待下次启动的时候再干了这就是本文要介绍的崩溃恢复过程。2. 读取两次写页面MySQL 一旦崩溃Redo 日志就要去拯救世界了MySQL 就是它的世界Redo 日志拯救世界的方式就是把还没来得及刷盘的脏页恢复到崩溃之前那一刻的状态。虽然 Redo 日志能够用来恢复数据页但这是有前提条件的数据页必须完好无损的状态。本文我们把系统表空间、独立表空间、undo 表空间中的页统称为数据页。如果数据页刚写了一半MySQL 就戛然而止这个数据页就损坏了面对这种情况Redo 日志也是巧妇难为无米之炊。Redo 日志拯救世界之路就要因为这个问题停滞不前吗那显示是不能的这就该轮到两次写上场了。两次写的官方名字是 double write它包含内存缓冲区和 dblwr 文件两个部分InnoDB 脏页刷盘前都会先把脏页写入内存缓冲区再写入 dblwr 文件成功之后才会把网页刷盘。两次写通过系统变量 innodb_doublewrite 控制开启或关闭本文内容基于该系统变量的默认值 ON表示开启两次写。如果网页写入内存缓冲区和 dblwr 文件的程中MySQL 崩溃了表空间中对应的数据页还是完整的下次启动时不需要用两次写页面修复这个数据页。如果脏页刷盘时MySQL 崩溃了表空间对应的数据页损坏了下次启动时应用 Redo 日志到数据页之前需要用两次写页面修复这个数据页。dblwr 文件 默认位于 MySQL 数据目录下[cschcsch /usr/local/mysql_8_0_29/data]ls-l|grepdblwr -rw-r-----1csch staff 192K82712:04#ib_16384_0.dblwr-rw-r-----1csch staff8.2M8116:29#ib_16384_1.dblwrMySQL 启动过程中会把 *.dblwr 文件中的所有两次写页面加载到两次写内存缓冲区并用内存缓冲区中的两次写页面修复损坏的数据页然后再应用 Redo 日志到数据页。3. 恢复数据页应用 Redo 日志到数据页3.4 小节需要先读取 Redo 日志3.3 小节。读取日志 Redo 日志需要有个起点起点就是最后一次 checkpoint 的 lsn3.1 小节。应用 Redo 日志有一个前提数据页必须是完好无损的。要保证数据页的完整性应用 Redo 日志之前需要修复损坏的数据页3.2 小节。修复损坏数据页只需要保证在应用 Redo 日志之前就行了之所以安排在 3.2 小节是遵循了源码中的顺序。了解本节安排内容顺序的逻辑有助于理解应用 Redo 日志恢复数据页的过程接下来我们正式进入下一个环节。3.1 找到 last_checkpoint_lsn读取 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 日志。3.2 修复损坏的数据页把两次写文件中的所有数据页都加载到内存缓冲区之后需要用这些页来把系统表空间、独立表空间、undo 表空间中损坏的数据页恢复到正常状态。正常状态指的是 MySQL 崩溃之前数据页最后一次正确的刷新到磁盘的状态。恢复数据页的过程是对两次写内存缓冲区中的所有数据页进行循环从两次写数据页中读取表空间 ID、页号然后根据表空间 ID 和页号去系统表空间、独立表空间、undo 表空间中读取对应的数据页。读取到对应的数据页之后会根据其 File Header、File Trailer 中的一些字段判断数据页是不是已经损坏了首先从 File Header 中读取 FILE_PAGE_LSN 字段如果 FILE_PAGE_LSN 字段值大于当前系统已经生成的 Redo 日志的最大 LSN说明数据库出现了不可描述的错误数据页已经损坏。然后从 File Header 中读取 FILE_PAGE_SPACE_OR_CHECKSUM 字段值从 File Trailer 的前 4 字节中读取 checksum。如果 FILE_PAGE_SPACE_OR_CHECKSUM 字段值和 File Trailer checksum 不一样说明数据页已经损坏。一旦出现了上面 2 种情况中的 1 种把两次写数据页的内容复制到对应的数据页中数据页就会恢复到正常状态了。3.2 读取 Redo 日志前面确定了读取 Redo 日志的起点 last_checkpoint_lsn接下来就该读取 Redo 日志了主要流程如下第 1 步InnoDB 会以 64K 为单位从 Redo 日志文件读取日志到 log buffer 中。64K 4 * innodb_page_size所以每次从 Redo 日志文件读取的数据量取决于系统变量 innodb_page_size。第 2 步已经读取到 log buffer 中的 block利用 block header 和 block tailer 中的信息对 block 进行完整性检验之后把 block body 信息拷贝到另一个缓冲区 parsing buffer。parsing buffer 是一个 2M 的固定大小缓冲区用于存放即将要被解析的 Redo 日志。Redo 日志每个 block 的大小为 512 字节block header 为 12 字节block trailer 为 4 字节。从 log buffer 的每个 block 中拷贝到 parsing buffer 的 block body 大小就是 512-12-4 496 字节也就是每个 block 中存放的 Redo 日志数据部分。第 3 步解析 parsing buffer 中的 Redo 日志。这一步解析 Redo 日志实际上只是个预处理操作并不会完整的解析每一条 Redo 日志而是只会解析每一条 Redo 日志中的头信息以及数据地址包括以 4 个部分Redo 日志类型Redo 日志所属数据页的表空间 IDRedo 日志所属数据页的页号Redo 日志数据这部分只是得到了每一条 Redo 日志在 block body 中的地址后面应用 Redo 日志到数据页时会用到。第 4 步把第 3 步解析出来的每一条 Redo 日志的 4 个部分都拷贝到 hash 表中。这个 hash 表是个嵌套结构第 1 层 hash key 是表空间 IDvalue 也是个 hash 结构也就是第 2 层。同一个表空间的 Redo 日志以页单位组织到一起存放到以表空间 ID 为 key 的第 1 层 hash value 中。第 2 层的 hash key 是页号value 是需要应用到这个数据页的 Redo 日志组成的链表。同一个数据页的 Redo 日志链表以页号为 key放在第 2 层 hash value 中。链表中的 Redo 日志按照产生的先后顺序排列第 1 条就是要应用的这些 Redo 日志中最早产生的那条。第 5 步应用 Redo 日志到数据页。如果第 4 步进行的过程中Redo 日志数据拷贝到 hash 表之后导致 hash 表占用的空间大于 max_memory那么需要应用 Redo 日志到数据页应用完成之后清空 hash 表为下一批 Redo 日志数据腾出空间。这里的 max_memory 表示 hash 表能够使用的最大内存空间。1 ~ 5 步是个循环执行过程经过 N 轮循环之后hash 表中有非常大的可能性还存在着最后一批 Redo 日志因为占用空间小于等于 max_memory 而只能在那里苦苦等待着被应用到 Redo 日志这个工作就要等待第 6 步去干了。第 6 步收尾工作。1 ~ 5 步循环结束之后收尾工作就把 hash 表中剩下的 Redo 日志应用到数据页这是崩溃过程中最后一次应用 Redo 日志。前面都没有提到过存放 Redo 日志的 hash 表在哪里能使用多大内存不知道你有没有好奇过这个 hash 表并不会单独申请一大块内存而是借用了 buffer pool 中的内存。因为在崩溃恢复过程中进行到读取 Redo 日志阶段时buffer pool 还没有真正开始用所以可以先借来给 hash 表用一下。不过 hash 表并不能使用 buffer pool 的全部内存而是需要保留一部分内存用于应用 Redo 日志到数据页的过程中加载数据页到 buffer pool 中。保留内存大小为buffer pool 实例数量 * 256 个数据页buffer pool 中的剩余内存就是第 5 步提到的 max_memory也就是 hash 表能够使用的最大内存。3.4 应用 Redo 日志前面介绍读取 Redo 日志为了流程的完整性有 2 个步骤已经涉及到应用 Redo 日志了。这里要介绍的是应用 Redo 日志的过程会比上一小节深入一些。读取 Redo 日志阶段已经把所有需要应用的 Redo 日志都进行过预处理并拷贝到 hash 表了。存放 Redo 日志的 hash 表是一个嵌套结构第 1 层的 hash key 是表空间 IDhash value 还是一个 hash 表。第 2 层的 hash key 是页号hash value 是个 Redo 日志链表链表中的每个元素就是一条需要应用的 Redo 日志按照产生的先后排序。把每个数据页的 Redo 日志汇总到一起再去应用 Redo 日志这样做的好处是效率高。在崩溃恢复过程中每个数据页只需要被加载到 buffer pool 中一次一个数据页的 Redo 日志能够一次性应用干脆利落。应用 Redo 日志就是循环这个嵌套的 hash 表把每一条 Redo 日志都应用到数据页中主要流程如下第 1 步从第 1 层 hash 表中取到表空间 ID 和这个 undo 表空间下需要应用的 Redo 日志组成的第 2 层 hash 表。第 2 步从第 2 层 hash 表中取到一个页号和该数据页中需要应用的 Redo 日志链表。第 3 步判断当前循环的数据页是不是已经加载到 buffer pool 中了。如果当前页没有加载到 buffer pool 中进入第 4 步。如果当前页已经加载到 buffer pool 中进入第 5 步。第 4 步把不在 buffer pool 中的数据页加载到 buffer pool 中。加载数据页到 buffer pool 中是一个异步的批量操作有可能会一次加载多个数据页。也就是说把数据页从表空间加载到 buffer pool 中会触发预读提前把一批需要应用 Redo 日志的数据页一次性加载到 buffer pool 中。预读的数据页不是随机读取的而是根据第 3 步判断不在 buffer pool 中的数据页的页号记为 page_no计算出一个页号范围把这个范围内需要应用 Redo 日志的数据页全都加载到 buffer pool 中。页号范围的起点low_limit page_no - page % 32终点low_limit 32。循环 low_limit ~ low_limit 32 范围内的页号只要碰到需要应用 Redo 日志的数据页就先把页号临时存放到一个数组里。循环结束后把数组里的页号对应的数据页异步批量加载到 buffer pool 中。从上面的逻辑可以看到一次预读最多只读 32 个数据页。第 5 步应用 Redo 日志到数据页。根据第 1 步取到的表空间 ID和第 2 步取到的页号从 hash 表中获取该数据页需要应用的 Redo 日志链表。从数据页的 File Header 中读取 FILE_PAGE_LSN循环 Redo 日志链表中的每一条日志判断该日志的 start_lsn 是否大于等于 FILE_PAGE_LSN。如果 start_lsn FILE_PAGE_LSN说明该 Redo 日志对应的操作修改的数据页在 MySQL 崩溃之前就已经刷盘该 Redo 日志就不需要应用到数据页了。如果 start_lsn FILE_PAGE_LSN说明该 Redo 日志需要应用到数据页。然后根据 Redo 日志类型调用不同的方法解析 Redo 日志直接修改 buffer pool 中的数据页对该数据页应用 Redo 日志的过程就完成了。1 ~ 5 步是个循环过程直到所有 undo 表空间的 Redo 日志都被应用到数据页循环过程结束。4. 删除 undo 表空间MySQL 运行过程中如果有大事务往 undo 表空间中写入大量 undo 日志undo 表空间会变大。在早期版本中undo 表空间变大之后就不能再缩回去了。现在如果系统变量 innodb_undo_log_truncate 设置为 on当 undo 表空间增长到 innodb_max_undo_log_size 设置的大小默认值为 1G之后InnoDB 会把这个 undo 表空间截断为初始大小16M。除了通过系统变量控制 undo 表空间自动截断之外还可以用下面这个 SQL 手动触发ALTER UNDO TABLESPACE tablespace_name SET INACTIVE不管自动还是手动有可能 InnoDB 正在进行 undo 表空间截断操作MySQL 就突然崩溃了截断表空间操作还没有完成那怎么办等到下次启动的时候InnoDB 需要把未完成的 undo 表空间截断操作继续完成。InnoDB 怎么知道哪些 undo 表空间的截断操作没有完成这就需要用到一个标记文件了InnoDB 对某个 undo 表空间进行截断操作之前会创建一个对应的标记文件文件名是这样的undo_表空间编号_trunc.log。解释一下表空间的两个标识表空间编号是给咱们人类看的表空间 ID 是 MySQL 内部使用的这两者不一样。以 undo_001 表空间为例表空间编号为 1InnoDB 对 undo_001 表空间进行截断操作之前会创建一个 undo_1_trunc.log 文件如下[cschcsch/usr/local/mysql_8_0_29/data]ls-l | grep undo-rw-r-----1csch staff 16M82712:04undo_001-rw-r-----1csch staff 16M82712:04undo_002-rw-r--r--1csch staff 16K62212:36undo_1_trunc.log崩溃恢复过程中InnoDB 如果发现某个表空间存在对应的 trunc.log 文件说明这个 undo 表空间在 MySQL 崩溃时正在进行截断操作。但是只通过 trunc.log 文件存在这一个条件并不能确定 undo 表空间截断操作没有完成还要进一步判断。接着读取 trunc.log 文件的内容把读到的内容转换成数字判断这个数字是不是等于 76845412。76845412 是什么稍候介绍。如果等于说明在 MySQL 崩溃之前undo 表空间截断操作已经完成只是 trunc.log 文件还没来得及删除。此时直接删除这个文件就可以了。如果不等于说明 MySQL 崩溃时undo 表空间截断操作还没有完成那就需要继续完成。此时直接删除 undo 表空间文件。被删除的 undo 表空间要等到初始化事务子系统之后才会重建重建过程我们稍后介绍。举个例子启动过程中发现了 undo_001 表空间对应的 trunc.log 文件并且文件中存储的数字不是 76845412那就直接删除 undo_001 表空间。删除之后就只有 undo_1_trunc.log 文件能证明 undo_001 表空间存在过了就像下面这样[cschcsch/usr/local/mysql_8_0_29/data]ls-l | grep undo-rw-r-----1csch staff 16M82712:04undo_002-rw-r--r--1csch staff 16K62212:36undo_1_trunc.log为什么这里不把 undo 表空间对应的 trunc.log 文件一起删除因为 undo 表空间要等到初始化事务子系统完成之后再重建而 trunc.log 是 undo 表空间重建的凭证所以现在还不能删除。接下来我们再看看 trunc.log 文件的创建和写入过程。InnoDB 进行 undo 表空间截断操作之前就会创建 trunc.log 文件大小为 innodb_page_size 字节并把文件内容的所有字节都初始化为 NULL然后开始进行 undo 表空间截断操作。操作完成之后会往 trunc.log 文件中写入一个被称为魔数的数字76845412用于标识 undo 表空间截断操作已经完成。如果魔数成功写入 trunc.log 文件接下来会把 trunc.log 文件删除undo 表空间的截断操作就结束了。5. 初始化事务子系统现在我们来到了初始化事务子系统阶段。InnoDB 之所以把初始化事务子系统安排在删除 undo 表空间之后有可能是为了避免读取要被删除的 undo 表空间能够节省一点点时间。删除还没有完成截断操作的 undo 表空间文件之后剩下的 undo 表空间文件都需要读取。从 undo 表空间文件读取未完成的事务初始化事务子系统主要过程如下初始化事务子系统还包含其它操作不在本文介绍的范围内。第 1 步从内存中的 undo 表空间对象数组中读取 undo 表空间信息。undo 表空间默认为 2 个最多可以有 127 个。有了独立 undo 表空间之后位于系统表空间中的回滚段就已经不再使用了所以不需要从系统表空间的回滚段中读取事务信息。第 2 步从 undo 表空间中页号 3 的数据页中读取回滚段。每个 undo 表空间可以有 1 ~ 128 个回滚段由系统变量 innodb_rollback_segments 控制默认值为 2.第 3 步从回滚段中读取 undo slot。回滚段的段头页中有 1024 个 undo slot4 字节每个 undo slot 对应一个 undo 段。如果 undo slot 的值 等于 FIL_NULL表示这个 undo slot 没有关联到 undo 段继续执行第 3 步读取下一个 undo slot。如果 undo slot 的值 不等于 FIL_NULL表示这个 undo slot 关联了 undo 段进入第 4 步。第 4 步从 undo slot 对应的 undo 段中读取未完成事务的信息。此时undo slot 的值就是 undo 段的段头页的页号通过这个页号可以读取到 undo 段中的事务信息。undo slot 关联了 undo 段说明数据库崩溃时undo 段中的事务还没有完成事务状态可能是以下 3 种之一TRX_STATE_ACTIVE表示事务还没有进入提交阶段。TRX_STATE_PREPARED表示事务已经提交了但是只完成了二阶段提交的 PREPARE 阶段还没有完成 COMMIT 阶段。TRX_STATE_COMMITTED_IN_MEMORY表示事务已经完成了二阶段提交的 2 个阶段还剩一些收尾工作没做这种状态的事务修改的数据已经可以被其它事务看见了。事务的收尾工作有哪些清理已提交事务小节会介绍。第 1 ~ 4 步是个循环的过程直到读完所有 undo 表空间中的事务信息结束。6. 重建 undo 表空间对于存在 trunc.log 文件的 undo 表空间因为之前 undo 表空间文件被删除了现在要开始着手重建 undo 表空间了主要流程如下第 1 步创建 trunc.log 文件标记 undo 表空间重建操作正在进行中。看到这里你可能会奇怪undo 表空间对应的 trunc.log 文件不是没有删除吗这里为什么又要创建一次别急且往下看。在创建 undo 表空间对应的 trunc.log 文件之前会先删除之前旧的 trunc.log 文件然后创建新的 trunc.log 文件。新旧 trunc.log 文件名是一样的例如对于 undo_001 表空间来说新旧 trunc.log 文件名都是 undo_1_trunc.log。为什么要删除旧的 trunc.log 文件再创建新的同名 trunc.log 文件呢因为重建 undo 表空间和新建 undo 表空间是同一套逻辑而新建 undo 表空间之前该表空间并不存在对应的 trunc.log 文件。为了保持统一的逻辑所以会先删除已经存在的 trunc.log 文件。第 2 步创建 undo 表空间文件初始大小为 16M这个大小是硬编码的。第 3 步初始化 undo 表空间把表空间 ID、各种链表信息写入表空间的 0 号页中然后分配一个新的数据页创建并初始化回滚段回滚段数量由系统变量 innodb_rollback_segments 控制。第 4 步循环 undo 表空间中的所有回滚段把每个回滚段中的 1024 个 undo slot 都初始化为 FIL_NULL。第 5 步标记 undo 表空间重建操作已经完成。InnoDB 会先往 trunc.log 文件中写入一个魔数 76845412表示重建表空间操作已经完成。写入魔数成功之后再把 trunc.log 文件删除重建一个 undo 表空间的过程就结束了。如果有多个 undo 表空间需要重建对于每个 undo 表空间都需要进行 1 ~ 5 步的流程。7. 处理事务在初始化事务子系统小节我们介绍过从 undo 表空间中读取出来的事务有 3 种状态TRX_STATE_ACTIVETRX_STATE_PREPAREDTRX_STATE_COMMITTED_IN_MEMORY处理事务阶段对这 3 种状态会进行不同的处理请接着往下看。7.1 清理已提交事务这里要清理的已提交事务指的是状态为 TRX_STATE_COMMITTED_IN_MEMORY 的事务包含 DDL 和 DML 事务。这种状态的事务已经完成二阶段提交的 PREPARE 和 COMMIT 阶段是已经提交成功的事务只差最后一点点清理工作它们修改的数据已经能被其它事务看见了。清理工作主要有几点处理 insert undo 段。如果 insert undo 段能被缓存undo 段会被加入 insert_undo_cached 链表尾部以备重复使用如果 insert undo 段不能被缓存undo 段就会被释放。把事务从读写事务链表中删除。把事务状态修改为 TRX_STATE_NOT_STARTED。7.2 回滚未提交 DDL 事务未提交事务指的是状态为 TRX_STATE_ACTIVE 的事务也就是活跃事务。崩溃恢复过程中这种状态的事务是需要直接回滚的。你可能会有个疑问DDL 事务不是不能回滚吗DDL 事务不能回滚这只是针对 MySQL 用户而言MySQL 内部并不会受到这个限制。我们在使用 MySQL 的过程中如果在一个 DML 事务中间执行了一条 DDL 语句会触发隐式提交直接把 DML 事务提交了。然后 DDL 会开启一个新事务这个新事务是自动提交的DDL 执行完成之后事务就直接提交了我们是没有机会对 DDL 事务进行回滚操作的。MySQL 没给我们回滚 DDL 事务的机会但是它自己有这个特权。7.3 回滚未提交 DML 事务未提交的 DDL 事务和 DML 事务在源码中是在不同时间触发的它的回滚过程和 DDL 事务一样。事务回滚的过程比较复杂本文我们就不展开说了后续会写一篇文章专门介绍事务回滚的过程。7.4 处理 PREPARE 事务PREPARE 事务指的是状态为 TRX_STATE_PREPARED 的事务这种状态的事务比较特殊在崩溃恢复过程中既有可能被提交也有可能被回滚。PREPARE 事务提交还是回滚取决于这个事务的 XID 是否已经写入到 binlog 日志文件中。事务 XID 是以 binlog event 的方式写入 binlog 日志文件的event 的名字是 XID_EVENT。一个事务只会有一个 XID也就只会有一个 XID_EVENT 了。要知道事务的 XID_EVENT 是否已经写入到 binlog 日志文件需要先读取 binlog 日志文件。从上面的介绍可以看到处理 PREPARE 事务依赖于 binlog 日志文件因此这部分逻辑是在打开 binlog 日志文件的过程中实现的。MySQL 在同一时刻只会往一个 binlog 日志文件中写入 binlog event在崩溃那一刻承载写入 event 的文件是最后一个 binlog 日志文件。因此崩溃恢复过程中只需要扫描最后一个 binlog 日志文件找到其中所有的 XID_EVENT 用于判断 PREPARE 事务的 XID_EVENT 是否已经写入 binlog 日志文件。如果 MySQL 上一次是正常关闭启动过程中不会存在没有完成的事务没有 PREPARE 事务需要处理也就不用扫描最后一个 binlog 日志文件了。MySQL 怎么知道上一次是不是正常关闭呢每个 binlog 日志文件的第 1 个 EVENT 都是 FORMAT_DESCRIPTION_EVENT用于描述 binlog 日志文件格式信息这个 EVENT 中包含一个标记 LOG_EVENT_BINLOG_IN_USE_F。binlog 日志文件创建时这个标记位会被设置为 1表示 binlog 日志文件正在被使用。LOG_EVENT_BINLOG_IN_USE_F 标记在 2 种情况下会被清除切换 binlog 日志文件时旧 binlog 日志文件的 LOG_EVENT_BINLOG_IN_USE_F 标记会被清除。MySQL 正常关闭时正在使用的 binlog 日志文件的 LOG_EVENT_BINLOG_IN_USE_F 标记会被清除。如果 MySQL 突然崩溃来不及把这个标记设置为 0。那么下次启动时MySQL 读取最后一个 binlog 日志文件的 FORMAT_DESCRIPTION_EVENT 发现 LOG_EVENT_BINLOG_IN_USE_F 标记为 1就会进入处理 PREPARE 事务阶段主要流程如下第 1 步扫描最后一个 binlog 日志文件读取 EVENT找到其中所有的 XID_EVENT并把读取到的事务 XID 存放到一个集合中。第 2 步InnoDB 循环读写事务链表每找到一个 PREPARE 事务都存放到数组中最后把数组返回给 server 层。第 3 步读取 InnoDB 返回的 PREPARE 事务数组判断事务 XID 是否在第 1 步的事务 XID 集合中。第 4 步提交或回滚事务。如果事务 XID 在集合中说明 MySQL 崩溃之前事务 XID_EVENT 就已经写入 binlog 日志文件了。XID_EVENT 有可能已经同步给从服务器从服务器上可能已经重放了这个事务。这种情况下为了保证主从数据的一致性事务在主服务器上也需要提交。如果事务 XID 不在集合中说明 MySQL 崩溃之前事务 XID_EVENT 没有写入 binlog 日志文件。XID_EVENT 肯定也就没有同步给从服务器了同样为了保证主从数据的一致性事务在主服务器上也不能提交而是需要回滚。3 ~ 4 步是个循环过程循环完 InnoDB 返回的 PREPARE 事务数组之后处理 PREPARE 事务的过程结束崩溃恢复主要流程也就完成了。8. 总结MySQL 崩溃恢复过程的核心工作有 2 点对于 MySQL 崩溃之前还没有刷新到磁盘的数据页也就是脏页用 Redo 日志把这些数据页恢复到 MySQL 崩溃之前那一刻的状态这相当于对脏页进行一次刷盘操作。在这之前需要用两次写缓冲区中的页把损坏的数据页修复为正常状态然后才能在此基础上用 Redo 日志恢复数据页。清理、提交、回滚还没有完成的事务。对于已完成二阶段提交的 PREPARE、COMMIT 2 个阶段的事务做收尾工作。对于活跃状态的事务直接回滚。对于 PREPARE 状态的事务如果事务 XID 已写入 binlog 日志文件提交事务否则回滚事务。