1.概述
上一篇介绍了重做日志写入相关原理,本文主要介绍如何从磁盘进行重做日志的恢复。做好数据的恢复,才能保证节点故障不会丢数据。
2.实现细节
每次innodb启动的时候都会尝试进行重做日志的恢复。
We always try to do a recovery, even if the database had been shut down normally: this is the normal startup path
核心就是通过检查点从磁盘中加载数据到内存。
log_checkpointer线程负责检查点的写入,有相应的判断算法, 本质就是已经持久化到磁盘的脏页对应的lsn会被写入检查点,如果系统故障,我们从此对应lsn恢复即可保证数据不丢失。
3.源码解析
fil_write_flushed_lsn_to_data_files
数据库正常结束之前,会调用该方法,将lsn写入表空间,该lsn主要用来判断是否需要进行数据的的恢复。如果正常结束,lsn和重做日志检查点一致(正常shutdown会将buffer pool刷新到磁盘并且更新检查点),就不需要进行数据恢复,如果不一致,则说明数据库异常关闭,则需要进行数据的恢复。
recv_recovery_from_checkpoint_start
数据恢复主要通过此方法实现
- 首先创建并初始化recv_sys数据结构,该数据结构主要用来数据的恢复。所有等待恢复的日志数据最终都先加载到redolog buf,再解析buf到recv_sys的哈希表中。最终通过哈希表存储的日志数据,来进行数据的恢复。为什么要用hash?以为对于相同页的数据,方便查找。
1 | cpp复制代码if (type == LOG_CHECKPOINT) { |
哈希表结构,n_cells为槽数量,array为槽数据,每个槽存放一个链表,解决hash冲突。链表的每个node存储对应块的日志信息。
1 | cpp复制代码struct hash_table_struct { |
- 查找最大的checkpoint
如上图,因为innodb会保存两个checkpoint,所以需要从所有group找到最大的那个。该方法比较简单,其实就是遍历所有group,从文件读取checkpoint信息到对应的checkpoint_buf。然后对数据进行一致性check。找到最大的checkpoint,返回该checkpoint对应的group和field。
1 | cpp复制代码for (field = LOG_CHECKPOINT_1; field <= LOG_CHECKPOINT_2; field += LOG_CHECKPOINT_2 - LOG_CHECKPOINT_1) { |
- log_group_read_checkpoint_info,根据返回的group和field,读取该checkpoint的信息。
1 | cpp复制代码void log_group_read_checkpoint_info( |
- recv_group_scan_log_recs
该方法主要是根据读取到的信息扫描并加载group保存的日志信息,最终插入到recv_sys的hash表中。整个流程如下:
(1)调用log_group_read_log_seg读取group日志到log_sys->buf中
1 | cpp复制代码while (!finished) |
(2)调用recv_scan_log_recs,循环处理log_sys->buf中所有log_block。
每次循环的逻辑如下:
先对日志的校验操作。如果校验不成功则恢复失败。对于校验通过的log_block,调用recv_sys_add_to_parsing_buf将log_block的buf数据拷贝到recv_sys的buf中。
recv_sys_add_to_parsing_buf方法逻辑:
主要是计算拷贝的start_offset和end_offset,然后进行调用memcpy方法拷贝。因为lsn包括了头部和尾部的数据,但是拷贝的时候只需要数据部分,所以需要进一步计算才可以。
(3)调用recv_parse_log_recs解析所有日志,并将日志存储到hash table。
主要就是遍历所有日志,根据日志规则解析出日志的type,space,page_no,然后调用recv_add_to_hash_table初始化hash结构并插入hash表中。
1 | cpp复制代码old_lsn = recv_sys->recovered_lsn; |
- recv_apply_hashed_log_recs
这个方法将hash table中的数据刷新到page中,进行日志的恢复。遍历所有的hashtable,对于在内存中的page,直接调用recv_recover_page进行恢复,对于不在内存中的页调用recv_read_in_area方法。
(1)先说recv_read_in_area这个方法
这个方法主要就是遍历该页所相邻的32个页,如果此页不在内存中,则将页编号其加入到page_nos数组,然后异步加载页并刷新数据。
1 | cpp复制代码static ulint recv_read_in_area( |
(2)recv_recover_page
页的恢复逻辑。启动mini事务,设置为非log模式,也就是恢复时候不需要再记录重做日志。
核心就是调用recv_parse_or_apply_log_rec_body。该方法是根据重做日志的类型进行不同的恢复操作。细节后续会说。写入完成之后,更新page的checksum以及lsn。并将其插入到flush列表等待刷新,最终提交mini事务。
总结
文章串了一下整个恢复的流程,根据检查点从redolog文件记载道redo log的buf,然后读取buf到recv_sys中。整个恢复操作围绕recv_sys开展,对于在内存中的页,直接刷新数据,对于不在内存的页异步去做。
本文转载自: 掘金