阅读 116

innodb重做日志实现原理(下)

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

数据恢复主要通过此方法实现

  1. 首先创建并初始化recv_sys数据结构,该数据结构主要用来数据的恢复。所有等待恢复的日志数据最终都先加载到redolog buf,再解析buf到recv_sys的哈希表中。最终通过哈希表存储的日志数据,来进行数据的恢复。为什么要用hash?以为对于相同页的数据,方便查找。

if (type == LOG_CHECKPOINT) {     recv_sys_create();     recv_sys_init(FALSE, buf_pool_get_curr_size()); } 复制代码

哈希表结构,n_cells为槽数量,array为槽数据,每个槽存放一个链表,解决hash冲突。链表的每个node存储对应块的日志信息。

struct hash_table_struct {     ibool        adaptive;/* TRUE if this is the hash table of the                 adaptive hash index */     ulint        n_cells;/* number of cells in the hash table */     hash_cell_t*    array;    /* pointer to cell array */     ulint        n_mutexes;/* if mutexes != NULL, then the number of                 mutexes, must be a power of 2 */     mutex_t*    mutexes;/* NULL, or an array of mutexes used to                 protect segments of the hash table */     mem_heap_t**    heaps;    /* if this is non-NULL, hash chain nodes for                 external chaining can be allocated from these                 memory heaps; there are then n_mutexes many of                 these heaps */     mem_heap_t*    heap;     ulint        magic_n; }; 复制代码

  1. 查找最大的checkpoint

redolog-group.png 如上图,因为innodb会保存两个checkpoint,所以需要从所有group找到最大的那个。该方法比较简单,其实就是遍历所有group,从文件读取checkpoint信息到对应的checkpoint_buf。然后对数据进行一致性check。找到最大的checkpoint,返回该checkpoint对应的group和field。

for (field = LOG_CHECKPOINT_1; field <= LOG_CHECKPOINT_2; field += LOG_CHECKPOINT_2 - LOG_CHECKPOINT_1) {             log_group_read_checkpoint_info(group, field);             if (!recv_check_cp_is_consistent(buf)) {                 goto not_consistent;             }             group->state = LOG_GROUP_OK;             group->lsn = mach_read_from_8(buf + LOG_CHECKPOINT_LSN);             group->lsn_offset = mach_read_from_4(buf + LOG_CHECKPOINT_OFFSET);             checkpoint_no = mach_read_from_8(buf + LOG_CHECKPOINT_NO);             if (ut_dulint_cmp(checkpoint_no, max_no) >= 0) {                 *max_group = group;                 *max_field = field;                 max_no = checkpoint_no;             }         not_consistent:             ; } 复制代码

  1. log_group_read_checkpoint_info,根据返回的group和field,读取该checkpoint的信息。

void log_group_read_checkpoint_info(     log_group_t*    group,    /* in: log group */     ulint        field)    /* in: LOG_CHECKPOINT_1 or LOG_CHECKPOINT_2 */ {     log_sys->n_log_ios++;     fil_io(OS_FILE_READ | OS_FILE_LOG, TRUE, group->space_id,             field / UNIV_PAGE_SIZE, field % UNIV_PAGE_SIZE,             OS_FILE_LOG_BLOCK_SIZE, log_sys->checkpoint_buf, NULL); } 复制代码

  1. recv_group_scan_log_recs

该方法主要是根据读取到的信息扫描并加载group保存的日志信息,最终插入到recv_sys的hash表中。整个流程如下:

(1)调用log_group_read_log_seg读取group日志到log_sys->buf

while (!finished) {     end_lsn = ut_dulint_add(start_lsn, RECV_SCAN_SIZE);     log_group_read_log_seg(LOG_RECOVER, log_sys->buf, group, start_lsn, end_lsn);     finished = recv_scan_log_recs(TRUE,     (buf_pool->n_frames - recv_n_pool_free_frames) * UNIV_PAGE_SIZE,     TRUE, log_sys->buf,     RECV_SCAN_SIZE, start_lsn,     contiguous_lsn, group_scanned_lsn);     start_lsn = end_lsn; } 复制代码

(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表中。

old_lsn = recv_sys->recovered_lsn; len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body); if (recv_sys->found_corrupt_log) {     recv_report_corrupt_log(ptr,                 type, space, page_no); } ut_a(len != 0); ut_a(0 == ((ulint)*ptr & MLOG_SINGLE_REC_FLAG)); recv_sys->recovered_offset += len; recv_sys->recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len); if (type == MLOG_MULTI_REC_END) {     /* Found the end mark for the records */     break; } if (store_to_hash) {     recv_add_to_hash_table(type, space, page_no, body, ptr + len, old_lsn, new_recovered_lsn); } ptr += len; 复制代码

  1. 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数组,然后异步加载页并刷新数据。

static ulint recv_read_in_area(     ulint    space,    /* in: space */     ulint    page_no)/* in: page number */ {     recv_addr_t* recv_addr;     ulint    page_nos[RECV_READ_AHEAD_AREA];     ulint    low_limit;     ulint    n;     low_limit = page_no - (page_no % RECV_READ_AHEAD_AREA);     n = 0;     for (page_no = low_limit; page_no < low_limit + RECV_READ_AHEAD_AREA;page_no++) {         recv_addr = recv_get_fil_addr_struct(space, page_no);         if (recv_addr && !buf_page_peek(space, page_no)) {             mutex_enter(&(recv_sys->mutex));             if (recv_addr->state == RECV_NOT_PROCESSED) {                 recv_addr->state = RECV_BEING_READ;                      page_nos[n] = page_no;                 n++;             }                          mutex_exit(&(recv_sys->mutex));         }     }     buf_read_recv_pages(FALSE, space, page_nos, n);     return(n); } 复制代码

(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开展,对于在内存中的页,直接刷新数据,对于不在内存的页异步去做。


作者:大远哥
链接:https://juejin.cn/post/7021537650792202276

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐