redo 日志的恢复
InnoDB中,redo日志的恢复(崩溃恢复) 是保证事务持久性的最后一道防线。当MySQL服务器意外崩溃后重启,InnoDB会通过重做(redo)那些在崩溃前已经提交但尚未完全写入磁盘的事务修改,从而将数据库恢复到崩溃前的状态。下面详细讲解redo日志恢复的完整过程。
1. 恢复的起点:从最近的Checkpoint开始
崩溃恢复的第一步是确定从哪个LSN开始扫描redo日志。这个起点就是最近一次成功的checkpoint对应的LSN。
- 每个redo日志文件组(
ib_logfile0、ib_logfile1…)的第一个文件的前2048字节中,存储了两个特殊的block:checkpoint1和checkpoint2。它们轮流记录每次checkpoint的信息。 - 每个checkpoint块包含:
LOG_CHECKPOINT_NO:checkpoint编号,每次递增。LOG_CHECKPOINT_LSN:该checkpoint时的checkpoint_lsn(即可以被覆盖的日志最大LSN,也是恢复的起点)。LOG_CHECKPOINT_OFFSET:checkpoint_lsn在日志文件组中的物理偏移量。
- 恢复时,InnoDB读取这两个块,比较
checkpoint_no,选择编号较大的那个(即最新的checkpoint)。然后从该块中取出checkpoint_lsn和对应的偏移量,作为恢复的起始位置。
为什么要从checkpoint开始?因为checkpoint之前的redo日志对应的脏页都已刷盘,无需再恢复;只有checkpoint之后的日志才可能对应尚未落盘的修改。
2. 恢复的终点:确定扫描的结束位置
接下来需要确定需要扫描到日志文件的哪个位置为止。由于redo日志是循环写入的,可能最后一个文件只写了一半,因此需要找到最后一个完整的block。
- redo日志以512字节的block为单位存储。每个block的头部(
log block header)有一个字段LOG_BLOCK_HDR_DATA_LEN,表示该block中实际使用的字节数(包括header,但header固定12字节,body部分最多496字节)。 - 如果
LOG_BLOCK_HDR_DATA_LEN等于512,说明该block已写满;如果小于512,说明这是当前写入的最后一个block(因为最后一个block可能未填满)。 - 恢复时,从起点开始顺序扫描block,直到遇到一个
LOG_BLOCK_HDR_DATA_LEN小于512的block,该block即为扫描的终点。之后的所有内容都是无效的。
3. 日志解析与分组:保证原子性
redo日志以**Mini-Transaction(mtr)**为单位生成。每个mtr对应一组原子操作,其产生的多条redo日志在恢复时必须作为一个整体应用,要么全部应用,要么全部丢弃。如何识别日志组的边界?
- 单日志组:如果mtr只产生一条redo日志,则该日志的
type字段的最高位(第8位)被设置为1,表示这是一个单日志组,没有结束标记。 - 多日志组:如果mtr产生多条redo日志,则在所有日志之后附加一条特殊类型的日志——
MLOG_MULTI_REC_END(type=31)。这条日志不包含实际数据,仅作为组的结束标记。
恢复解析器按顺序读取redo日志,遵循以下规则:
- 读取一条日志,检查其
type的最高位。 - 如果最高位为1,则立即将该日志作为一个独立的组,准备应用。
- 如果最高位为0,则继续读取后续日志,直到遇到一条
MLOG_MULTI_REC_END,然后将从上次组结束之后到这条MLOG_MULTI_REC_END之间的所有日志作为一个组。 - 如果扫描到终点(最后一个block的末尾)仍未遇到
MLOG_MULTI_REC_END,则该组日志不完整,整个组被丢弃。
这样保证了每个mtr的原子性,不会出现部分应用的情况。
4. 应用日志:恢复数据页
解析出完整的日志组后,需要将日志中的修改应用到对应的数据页上。为了提高恢复效率,InnoDB采用了以下优化:
4.1 按页面分组(哈希表)
- 在恢复过程中,首先将所有需要应用的redo日志按表空间ID(space_id)和页号(page_no)进行分组,放到一个哈希表中。同一个页面的所有日志会被链接在一起,按LSN递增的顺序排列。
- 这样做的好处是:可以一次性将一个页面的所有修改重做完毕,避免反复随机读取同一个页面,大大减少I/O次数。
4.2 跳过已刷盘的页面
在真正重做某个页面的修改前,需要检查该页面当前在磁盘上的版本是否已经包含了这部分修改。每个数据页的头部都有一个FIL_PAGE_LSN字段,记录了该页最近一次修改对应的LSN(即newest_modification)。
- 对于即将应用的日志组中的第一条日志(记为日志LSN),如果目标页面的
FIL_PAGE_LSN大于等于 这条日志的LSN,说明该页面在崩溃前已经刷盘到至少这个LSN,那么整个页面的所有后续修改都已落盘,可以跳过整个页面的恢复。 - 如果
FIL_PAGE_LSN小于日志LSN,则需要从磁盘读取该页(如果尚未在Buffer Pool中),然后按照日志的顺序依次重做修改。
4.3 重做修改
- 对于每条redo日志,根据其类型调用相应的恢复函数。例如,
MLOG_COMP_REC_INSERT会调用函数在页面的指定位置插入一条记录;MLOG_8BYTE会直接在页面的偏移量处写入8个字节的数据。 - 重做过程中,可能会更新页面内的各种结构(如页目录、槽信息、链表指针等),确保页面恢复到崩溃前的状态。
4.4 写回磁盘
- 重做完成后的页面会被标记为脏页,并加入Buffer Pool的flush链表,等待后台线程或后续的checkpoint将其刷回磁盘。注意,崩溃恢复期间本身并不会主动刷盘,恢复只是将页面加载到内存并修改,后续的正常运行会处理刷盘。
5. 恢复完成后的状态
当所有需要恢复的redo日志应用完毕后,数据库就恢复到了崩溃前的一致状态。此时:
- 所有已提交事务的修改都已体现在数据页中(可能在内存或磁盘)。
- 所有未提交事务的修改,由于它们对应的redo日志可能已经写入(但事务未提交),但这些日志在恢复时也会被应用(因为恢复时不知道事务是否提交),因此还需要通过undo日志来回滚未提交的事务。注意:redo日志恢复只保证物理一致性,而逻辑上的事务回滚需要依赖undo日志,这通常在redo恢复完成后进行。
6. 相关参数的影响
innodb_force_recovery:当系统严重损坏时,可以通过该参数强制启动,但会跳过某些恢复步骤,可能导致数据丢失或损坏。通常设置为0(正常恢复),除非特殊情况。innodb_log_file_size和innodb_log_files_in_group:日志文件越大,可能包含的未刷盘数据越多,恢复时间越长。但频繁的checkpoint也会影响性能,需要权衡。innodb_fast_shutdown:正常关闭时,如果设置为1或2,可能不会执行完整的purge和插入缓冲合并,但checkpoint仍会执行,因此恢复时间通常可控。
7. 总结
redo日志的恢复过程是InnoDB崩溃恢复的核心步骤,其关键点可以概括为:
- 从最近的checkpoint开始扫描,避免处理已经刷盘的日志。
- 按block解析并识别日志组边界,保证每个mtr的原子性。
- 将日志按页面分组,利用哈希表减少随机I/O。
- 利用页面的
FIL_PAGE_LSN跳过已刷盘的页面,避免重复工作。 - 顺序重做日志,恢复页面到崩溃前的状态。
通过这套机制,InnoDB能够在保证数据一致性的前提下,尽可能高效地完成崩溃恢复,最大限度地减少服务不可用时间。理解这一过程有助于我们诊断恢复性能问题,并合理配置相关参数。