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
数据恢复主要通过此方法实现
首先创建并初始化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; }; 复制代码
查找最大的checkpoint
如上图,因为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: ; } 复制代码
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); } 复制代码
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; 复制代码
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