MySQL redo log恢復(fù)原理 | StoneDB技術(shù)分享會 #5

640.gif
640.jpg

設(shè)計(jì):小艾

審核:丁奇、李浩

責(zé)編:宇亭

作者:羅中天

浙江大學(xué)-軟件工程-在讀碩士折欠、StoneDB 內(nèi)核研發(fā)實(shí)習(xí)生

2023 年 StoneDB 開源之夏項(xiàng)目中選學(xué)生

redo log 類型

innodb 的 redo log 是帶有邏輯意義的物理日志:物理指的是 redo log 是針對某一個頁來說的簸喂,每條 redo log 都會有 Type唉俗、Space ID断国、Page Number 等信息嗤锉,如下圖所示源祈;邏輯指的是一條 redo log 中可能描述的不是在頁面上的某個偏移量的位置上寫入若干個字節(jié)的數(shù)據(jù)煎源,而是描述在頁面上插入或者刪除一條什么樣的記錄。
redo log 的通用結(jié)構(gòu)為

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">Type (1) + Space ID (4) + Page Number (4) + Body</pre>

Type 的最高位是一個 Single Record Flag 標(biāo)志位香缺,如果為 1手销,表示該 redo log 單獨(dú)構(gòu)成一個 mtr。
redo log 根據(jù)作用的對象图张,又可以分為作用于 Page 的 redo log锋拖,作用于 space 的 redo log 和提供額外信息的 redo log。

作用于 page 的 redo log

大多數(shù)的 redo log 屬于這一類別埂淮,常見的有 MLOG_1BYTE姑隅、MLOG_2BYTES、MLOG_4BYTES倔撞、MLOG_8BYTES讲仰、MLOG_REC_INSERT、MLOG_REC_CLUST_DELETE_MARK痪蝇、MLOG_REC_UPDATE_IN_PLACE 等鄙陡。其中 MLOG_1BYTE、MLOG_2BYTES躏啰、MLOG_4BYTES趁矾、MLOG_8BYTES 描述了在頁面的某個偏移量處寫入若干個字節(jié)的數(shù)據(jù);MLOG_REC_INSERT 描述了在頁面上插入一條記錄给僵;MLOG_REC_CLUST_DELETE_MARK 描述了在聚簇索引的頁面上刪除一條記錄(用戶線程刪除的操作只會打 delete 標(biāo)記毫捣,物理刪除的操作由 purge 線程來做);MLOG_REC_UPDATE_IN_PLACE 描述了在聚簇索引的頁面上原地更新一條記錄(即修改的是非索引列的字段帝际,二級索引上的更新不會產(chǎn)生該條日志蔓同,因?yàn)槎壦饕系挠涗洓]有版本鏈,所以更新操作產(chǎn)生的 redo log 為 MLOG_REC_CLUST_DELETE_MARK + MLOG_REC_INSERT)蹲诀。

MLOG_REC_INSERT

MLOG_REC_INSERT 類型的 redo log body 部分的格式為

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">version (1): 版本信息flag (1)n (2): 字段的數(shù)量inst_cols (2)n_uniq (2): 主鍵的數(shù)量n個字段的長度 (n * 2)offset (2): 前一條記錄在頁面中的偏移量end_seg_len (compressed): 從mismatch_index開始的記錄長度斑粱,最低位是標(biāo)志位info_and_status_bits (1)origin_offset (compressed): record header的長度mismatch_index (compressed): 和前一個記錄相比第一個不一樣的位置data (end_seg_len >> 1): 該redo log對應(yīng)的記錄從mismatch_index開始的數(shù)據(jù)</pre>

可見,MLOG_REC_INSERT 類型的 redo log 進(jìn)行了前綴壓縮

MLOG_REC_CLUST_DELETE_MARK

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">version (1): 版本信息flag (1)n (2): 字段的數(shù)量inst_cols (2)n_uniq (2): 主鍵的數(shù)量n個字段的長度 (n * 2)flags (1)val (1): 設(shè)置還是取消delete flagpos (compressed): trx_id在記錄中的偏移量roll_ptr (7)trx_id (compressed)offset (2): 記錄origin offset的位置在頁面中的偏移量</pre>

MLOG_REC_UPDATE_IN_PLACE

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">version (1): 版本信息flag (1)n (2): 字段的數(shù)量inst_cols (2)n_uniq (2): 主鍵的數(shù)量n個字段的長度 (n * 2)flags (1)pos (compressed): trx_id在記錄中的偏移量roll_ptr (7)trx_id (compressed)rec_offset (2): 記錄在頁面中的偏移量info_bits (1)n_fields (compressed): 修改的字段的數(shù)量對n_fields個修改字段的描述 field_no (compressed): 字段的編號 len (compressed): 字段的長度 data (len): 數(shù)據(jù)</pre>

作用于 space 的 redo log

這類 redo log 描述的是針對一個 space 文件的修改脯爪,由于這類文件不是 write ahead 的则北,而是在文件操作后才記錄的矿微,所以在恢復(fù)的過程中只會對于文件的狀態(tài)做一些檢查。這類 rede log 不是本文的重點(diǎn)尚揣,在后續(xù)不再贅述涌矢。

提供額外信息的 redo log

這一類的 redo log 主要指的是 MLOG_MULTI_REC_END,只由一個字節(jié)的 Type 構(gòu)成惑艇,用于標(biāo)識一個 mini transaction(簡稱 mtr)的結(jié)尾蒿辙。

recovery 原理

innodb 的 recovery 從 innodb 啟動的時(shí)候開始執(zhí)行,大概流程如下:
1滨巴、從 ib_logfile 文件的 header 中找到 checkpoint lsn,作為 recovery 的起點(diǎn)
2俺叭、每次從 ib_logfile 文件中讀取 64KB 的 redo log 到內(nèi)存中
3恭取、將每個 log block 的 header 和 trail 去掉后,拼出一份連續(xù)的日志
4熄守、以 mtr 為單位進(jìn)行解析
4.1蜈垮、判斷 MLOG_SINGLE_REC_FLAG 標(biāo)志位,如果一個 mtr 只由單條日志構(gòu)成裕照,直接解析后放入哈希表攒发;
4.2、如果一個 mtr 由多條日志構(gòu)成晋南,需要先找到 MLOG_MULTI_REC_END 類型的日志惠猿,確定 mtr 的終點(diǎn),并加入緩存中负间,然后將緩存中所有的日志都放入哈希表中
5偶妖、將哈希表中的 redo log 進(jìn)行重放
note:這里不直接在解析的時(shí)候回放,而是插入哈希表中回放的好處是:可能會有很多 redo log 作用在同一個 page 上政溃,將這些 redo log 使用一次 IO 進(jìn)行重放趾访,可以加快重放的速度。該哈希表包括兩層董虱,第一層以 space_id 為 key扼鞋,第二層以 page_no 為 key。
調(diào)用棧如下所示(下面的源碼基于 MySQL8.0.30 版本)

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">// storage/innobase/srv/srv0start.ccsrv_start // 從系統(tǒng)表的第一個頁中獲取flushed_lsn // 如果是正常shutdown的話愤诱,會做一次同步的全量checkpoint云头,會在系統(tǒng)表的第一個頁中寫入checkpoint的lsn srv_sys_space.open_or_create(false, create_new_db, &sum_of_new_sizes, &flushed_lsn); read_lsn_and_check_flags(flush_lsn); it->validate_first_page(it->m_space_id, flushed_lsn, false); flush_lsn = mach_read_from_8(m_first_page + FIL_PAGE_FILE_FLUSH_LSN); recv_recovery_from_checkpoint_start(log_sys, flushed_lsn); // 每個ib_logfile文件有2KB的header,在header的第2個log block和第4個log block中的8字節(jié)偏移量處分別存有checkpoint1和checkpoint2 // 當(dāng)checkpoint_no為偶數(shù)時(shí)转锈,寫入checkpoint1盘寡,為奇數(shù)時(shí),寫入checkpoint2 // 遍歷所有的ib_logfile文件撮慨,分別從其header中取出兩個checkpoint lsn竿痰,取最大值返回 // note: 其實(shí)在第一個ib_logfile中尋找checkpoint lsn即可脆粥,因?yàn)樽鯿heckpoint的時(shí)候只會往第一個ib_logfile中寫入 Log_checkpoint_location checkpoint; recv_find_max_checkpoint(log, checkpoint) // 從checkpoint lsn開始解析redo log并且apply recv_recovery_begin recv_read_log_seg recv_scan_log_recs recv_parse_log_recs recv_single_rec recv_parse_log_rec mlog_parse_initial_log_record recv_parse_or_apply_log_rec_body recv_multi_rec recv_parse_log_rec mlog_parse_initial_log_record recv_parse_or_apply_log_rec_body // 將哈希表中的redo log進(jìn)行重放 recv_apply_hashed_log_recs</pre>

下面對從 recv_recovery_begin 開始的流程進(jìn)行詳細(xì)闡述,在解析 redo log 的時(shí)候以解析 MLOG_REC_INSERT 類型的 redo log 為例進(jìn)行分析影涉。為了突出主干变隔,對代碼做了簡化。
innodb 將解析和重放的邏輯是寫在一起的蟹倾,當(dāng)傳入的 block 為空時(shí)匣缘,只解析不重放,當(dāng)傳入的 block 非空時(shí)鲜棠,解析并且重放肌厨。

recv_recovery_begin

該函數(shù)負(fù)責(zé)循環(huán)從 ib_logfile 文件中讀取 64KB 的 redo log 到內(nèi)存中進(jìn)行解析,并放入哈希表中

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">// storage/innobase/log/log0recv.ccstatic dberr_t recv_recovery_begin(log_t &log, const lsn_t checkpoint_lsn) { // 初始化recv_sys recv_sys->len = 0; ... // checkpoint_lsn向下向512KB對齊 lsn_t start_lsn = ut_uint64_align_down(checkpoint_lsn, OS_FILE_LOG_BLOCK_SIZE); bool finished = false; // 循環(huán)讀取ib_logfile中的內(nèi)容到 while (!finished) { // 讀取從start_lsn開始的64KB的數(shù)據(jù)到log.buf中 const lsn_t end_lsn = recv_read_log_seg(log, log.buf, start_lsn, start_lsn + RECV_SCAN_SIZE); if (end_lsn == start_lsn) { /* This could happen if we crashed just after completing file, and before next file has been successfully created. */ break; } dberr_t err; finished = recv_scan_log_recs(log, max_mem, log.buf, end_lsn - start_lsn, start_lsn, &log.m_scanned_lsn, err); if (err != DB_SUCCESS) { return err; } start_lsn = end_lsn; } return DB_SUCCESS;}</pre>

recv_read_log_seg

該函數(shù)負(fù)責(zé)從 ib_logfile 文件中讀取 64KB 的 redo log 到內(nèi)存中豁陆。

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">// storage/innobase/log/log0recv.ccstatic lsn_t recv_read_log_seg(log_t &log, byte buf, lsn_t start_lsn, const lsn_t end_lsn) { // 找到start_lsn所在的ib_logfile文件 auto file = log.m_files.find(start_lsn); if (file == log.m_files.end()) { / Missing valid file ! */ return start_lsn; } do { os_offset_t source_offset; // 計(jì)算start_lsn在ib_logfile文件中的偏移量 // LOG_FILE_HDR_SIZE + (lsn - file_start_lsn); source_offset = file->offset(start_lsn); os_offset_t len = end_lsn - start_lsn; bool switch_to_next_file = false; if (source_offset + len > file->m_size_in_bytes) { len = file->m_size_in_bytes - source_offset; switch_to_next_file = true; } // 讀取文件 const dberr_t err = log_data_blocks_read(file_handle, source_offset, len, buf); start_lsn += len; buf += len; if (switch_to_next_file) { // 切換到下一個文件 ... } } while (start_lsn != end_lsn); return end_lsn;}// 每個ib_logfile文件的header中記錄有該文件起始的file_start_lsnos_offset_t offset(lsn_t lsn) os_offset_t offset(lsn_t lsn, lsn_t file_start_lsn) return LOG_FILE_HDR_SIZE + (lsn - file_start_lsn);</pre>

recv_scan_log_recs

該函數(shù)先將每個 log block 的 header 和 trail 去掉后柑爸,拼出一份連續(xù)的日志,然后以 mtr 為單位進(jìn)行解析

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">struct Log_data_block_header { ... /** Offset up to which this block has data inside, computed from the beginning of the block. / // 該log block中前m_data_len個字節(jié)是有內(nèi)容的 uint16_t m_data_len; /* Offset to the first mtr starting in this block, or 0 if there is no mtr starting in this block. */ // 該log block中第一個從該block中開始的mtr的起始位置 uint16_t m_first_rec_group;};static bool recv_scan_log_recs(log_t &log, size_t max_memory, const byte *buf, size_t len, lsn_t start_lsn, lsn_t *read_upto_lsn, dberr_t &err) { const byte log_block = buf; lsn_t scanned_lsn = start_lsn; bool finished = false; bool more_data = false; // 每個log block有header和trail盒音,導(dǎo)致跨block的日志是不連續(xù)的表鳍,不能直接解析 // 所以需要先將每個block的header和trail去掉,將所有block的主體內(nèi)容拼起來 do { // 解析log block header Log_data_block_header block_header; log_data_block_header_deserialize(log_block, block_header); ... const auto data_len = block_header.m_data_len; ... // 如果解析redo log的起點(diǎn)位置還沒確定并且存在mtr從該block中開始祥诽,就確定解析的起點(diǎn) if (!recv_sys->parse_start_lsn && block_header.m_first_rec_group > 0) { recv_sys->parse_start_lsn = scanned_lsn + block_header.m_first_rec_group; if (recv_sys->parse_start_lsn < recv_sys->checkpoint_lsn) { recv_sys->bytes_to_ignore_before_checkpoint = recv_sys->checkpoint_lsn - recv_sys->parse_start_lsn; } recv_sys->scanned_lsn = recv_sys->parse_start_lsn; recv_sys->recovered_lsn = recv_sys->parse_start_lsn; } scanned_lsn += data_len; if (scanned_lsn > recv_sys->scanned_lsn) { // buf空間不夠用譬圣,擴(kuò)容 if (recv_sys->len + 4 * OS_FILE_LOG_BLOCK_SIZE >= recv_sys->buf_len) { recv_sys_resize_buf(); } if (!recv_sys->found_corrupt_log) { // 將該log block去掉header和trail后接到recv_sys->buf的尾部 more_data = recv_sys_add_to_parsing_buf(log_block, scanned_lsn); } recv_sys->scanned_lsn = scanned_lsn; } // 該log block沒有滿,那么解析redo log的終點(diǎn)就是這個block if (data_len < OS_FILE_LOG_BLOCK_SIZE) { / Log data for this group ends here / finished = true; break; } else { log_block += OS_FILE_LOG_BLOCK_SIZE; } } while (log_block < buf + len); if (more_data && !recv_sys->found_corrupt_log) { // 解析redo log recv_parse_log_recs(); if (recv_sys->recovered_offset > recv_sys->buf_len / 4) { / Move parsing buffer data to the buffer start */ recv_reset_buffer(); } } return finished;}</pre>

recv_parse_log_recs

該函數(shù)判斷 MLOG_SINGLE_REC_FLAG 標(biāo)志位雄坪,根據(jù)一個 mtr 是由一條日志組成還是多條日志組成厘熟,分開處理。

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">static void recv_parse_log_recs() { ut_ad(recv_sys->parse_start_lsn != 0); // 解析redo log以mtr為基本單位 for (;;) { byte ptr = recv_sys->buf + recv_sys->recovered_offset; byte end_ptr = recv_sys->buf + recv_sys->len; if (ptr == end_ptr) { return; } bool single_rec; switch (ptr) { case MLOG_DUMMY_RECORD: single_rec = true; break; default: // 解析Type最高位的標(biāo)志位诸衔,看該mtr是由單條redo log構(gòu)成還是多條redo log構(gòu)成 single_rec = !!(ptr & MLOG_SINGLE_REC_FLAG); } if (single_rec) { if (recv_single_rec(ptr, end_ptr)) { // 單條redo log構(gòu)成的mtr的解析入口 return; } } else if (recv_multi_rec(ptr, end_ptr)) { // 多條redo log構(gòu)成的mtr的解析入口 return; } }}</pre>

recv_single_rec

單條 redo log 構(gòu)成的 mtr 的解析盯漂,將單條 redo log 解析后插入到哈希表中。

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">static bool recv_single_rec(byte *ptr, byte *end_ptr) { lsn_t old_lsn = recv_sys->recovered_lsn; byte body; mlog_id_t type; page_no_t page_no; space_id_t space_id; // 解析單條redo log ulint len = recv_parse_log_rec(&type, ptr, end_ptr, &space_id, &page_no, &body); lsn_t new_recovered_lsn; new_recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len); if (new_recovered_lsn > recv_sys->scanned_lsn) { / The log record filled a log block, and we require that also the next log block should have been scanned in */ return true; } ... recv_sys->recovered_offset += len; recv_sys->recovered_lsn = new_recovered_lsn; if (recv_recovery_on) { // 將redo log加入到哈希表中 // 不直接重放的原因是可能會有很多redo log作用在同一個page上笨农,將這些redo log使用一次IO進(jìn)行重放就缆,可以加快重放的速度 // 哈希表包括兩層,第一層以space_id為key谒亦,第二層以page_no為key recv_add_to_hash_table(type, space_id, page_no, body, ptr + len, old_lsn, recv_sys->recovered_lsn); } return false;}</pre>

recv_multi_rec

多條 redo log 構(gòu)成的 mtr 的解析竭宰。
先確定 mtr 的重點(diǎn),并將解析好的 redo log 加入緩存中份招,遍歷該 mtr 中所有的 redo log切揭,從緩存中取出后插入到哈希表中。

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">static bool recv_multi_rec(byte *ptr, byte *end_ptr) { ulint n_recs = 0; ulint total_len = 0; // 先找到mtr的終點(diǎn)锁摔,即MLOG_MULTI_REC_END類型的記錄 for (;;) { mlog_id_t type = MLOG_BIGGEST_TYPE; byte *body; page_no_t page_no = 0; space_id_t space_id = 0; ulint len = recv_parse_log_rec(&type, ptr, end_ptr, &space_id, &page_no, &body); // 將部分解析的redo log緩存起來 recv_sys->save_rec(n_recs, space_id, page_no, type, body, len); total_len += len; ++n_recs; ptr += len; if (type == MLOG_MULTI_REC_END) { break; } } lsn_t new_recovered_lsn = recv_calc_lsn_on_data_add(recv_sys->recovered_lsn, total_len); // 重置ptr的位置廓旬,開始掃第二遍 ptr = recv_sys->buf + recv_sys->recovered_offset; for (ulint i = 0; i < n_recs; i++) { lsn_t old_lsn = recv_sys->recovered_lsn; space_id_t space_id = 0; page_no_t page_no = 0; mlog_id_t type = MLOG_BIGGEST_TYPE; byte *body = nullptr; size_t len = 0; // 從第一遍掃的緩存中取出一條redo log recv_sys->get_saved_rec(i, space_id, page_no, type, body, len); recv_sys->recovered_offset += len; recv_sys->recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len); if (recv_recovery_on) { // 將redo log加入到哈希表中 recv_add_to_hash_table(type, space_id, page_no, body, ptr + len, old_lsn, new_recovered_lsn); } ptr += len; } return false;}</pre>

recv_parse_log_rec

該函數(shù)負(fù)責(zé)對單條 redo log 日志進(jìn)行解析,先解析 Type谐腰、Space ID孕豹、Page Number涩盾,再解析 body

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">static ulint recv_parse_log_rec(mlog_id_t *type, byte *ptr, byte *end_ptr, space_id_t *space_id, page_no_t *page_no, byte **body) { byte *new_ptr; body = nullptr; switch (ptr) { case MLOG_MULTI_REC_END: case MLOG_DUMMY_RECORD: *page_no = FIL_NULL; *space_id = SPACE_UNKNOWN; type = static_cast<mlog_id_t>(ptr); return 1; ... } // 解析Type、Space ID励背、Page Number new_ptr = mlog_parse_initial_log_record(ptr, end_ptr, type, space_id, page_no); *body = new_ptr; if (new_ptr == nullptr) { return 0; } // 解析body部分 new_ptr = recv_parse_or_apply_log_rec_body( *type, new_ptr, end_ptr, *space_id, *page_no, nullptr, nullptr, new_ptr - ptr, recv_sys->recovered_lsn); if (new_ptr == nullptr) { return 0; } return new_ptr - ptr;}</pre>

mlog_parse_initial_log_record

該函數(shù)負(fù)責(zé)解析 Type春霍、Space ID、Page Number

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">// storage/innobase/mtr/mtr0log.ccbyte *mlog_parse_initial_log_record( const byte ptr, /!< in: buffer */ const byte end_ptr, /!< in: buffer end */ mlog_id_t type, /!< out: log record type: MLOG_1BYTE, ... */ space_id_t space, /!< out: space id */ page_no_t page_no) /!< out: page number /{ if (end_ptr < ptr + 1) { return (nullptr); } // 解析Type type = (mlog_id_t)((ulint)ptr & ~MLOG_SINGLE_REC_FLAG); ut_ad(type <= MLOG_BIGGEST_TYPE); ptr++; if (end_ptr < ptr + 2) { return (nullptr); } // 解析Space ID *space = mach_parse_compressed(&ptr, end_ptr); if (ptr != nullptr) { // 解析Page Number *page_no = mach_parse_compressed(&ptr, end_ptr); } return (const_cast<byte *>(ptr));}</pre>

recv_parse_or_apply_log_rec_body

該函數(shù)負(fù)責(zé)解析 body叶眉,枚舉所有的 type 類型址儒,分別進(jìn)行處理。
這里以 MLOG_REC_INSERT 的日志為例衅疙,會先解析字段數(shù)量莲趣、主鍵數(shù)量、字段長度等信息炼蛤,構(gòu)建出索引字典妖爷,然后解析剩余的部分,構(gòu)建出完整的記錄理朋,最后插入對應(yīng)的頁中。

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">// storage/innobase/log/log0recv.ccstatic byte *recv_parse_or_apply_log_rec_body( mlog_id_t type, byte *ptr, byte *end_ptr, space_id_t space_id, page_no_t page_no, buf_block_t *block, mtr_t *mtr, ulint parsed_bytes, lsn_t start_lsn) { ... dict_index_t *index = nullptr; ... // 這里枚舉了所有的redo log類型 switch (type) { ... case MLOG_REC_INSERT: // 解析字段數(shù)量绿聘、主鍵數(shù)量嗽上、字段長度等信息,構(gòu)建出索引字典 if (nullptr != (ptr = mlog_parse_index(ptr, end_ptr, &index))) { // 解析剩余的部分熄攘,構(gòu)建出完整的記錄兽愤,插入到對應(yīng)的頁中 ptr = page_cur_parse_insert_rec(false, ptr, end_ptr, block, index, mtr); } break; ... } if (index != nullptr) { dict_table_t *table = index->table; dict_mem_index_free(index); dict_mem_table_free(table); } return ptr;}</pre>

mlog_parse_index

該函數(shù)負(fù)責(zé)解析字段的數(shù)量,主鍵的數(shù)量和每個字段的長度挪圾,構(gòu)建索引字典

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">byte *mlog_parse_index(byte *ptr, const byte *end_ptr, dict_index_t *index) { / Read the 1 byte for index log version / uint8_t index_log_version = 0; ptr = parse_index_log_version(ptr, end_ptr, index_log_version); / Read the 1 byte flag / uint8_t flag = 0; ptr = parse_index_flag(ptr, end_ptr, flag); / Read n and n_uniq / // 解析字段的數(shù)量n和主鍵的數(shù)量n_uniq uint16_t n = 0; uint16_t n_uniq = 0; uint16_t inst_cols = 0; ptr = parse_index_column_counts(ptr, end_ptr, is_comp, is_versioned, is_instant, n, n_uniq, inst_cols); / Create a dummy dict_table_t */ dict_table_t table = dict_mem_table_create(RECOVERY_INDEX_TABLE_NAME, DICT_HDR_SPACE, n, 0, 0, is_comp ? DICT_TF_COMPACT : 0, 0); / Create a dummy dict_index_t */ dict_index_t ind = dict_mem_index_create(RECOVERY_INDEX_TABLE_NAME, RECOVERY_INDEX_TABLE_NAME, DICT_HDR_SPACE, 0, n); ind->table = table; ind->n_uniq = (unsigned int)n_uniq; if (n_uniq != n) { ind->type = DICT_CLUSTERED; } / Read each index field info */ // 解析每個字段的長度浅萧,填充index的feild信息 ptr = parse_index_fields(ptr, end_ptr, n, n_uniq, is_versioned, ind, table); if (ptr == nullptr) { *index = ind; return ptr; } ... *index = ind; return (ptr);}</pre>

parse_index_fields

該函數(shù)負(fù)責(zé)解析每個字段的長度,填充索引的 field 列表

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">static byte *parse_index_fields(byte *ptr, const byte *end_ptr, uint16_t n, uint16_t n_uniq, bool is_versioned, dict_index_t *&ind, dict_table_t &table) { for (size_t i = 0; i < n; i++) { uint16_t len = 0; // 讀取字段的長度信息 ptr = read_2_bytes(ptr, end_ptr, len); // 這里構(gòu)建出來的field字段的類型并不是準(zhǔn)確的哲思,只能區(qū)分出是變長還是定長洼畅,因?yàn)閞edo log中只有字段長度相關(guān)的信息,并沒有類型相關(guān)的信息 dict_mem_table_add_col( table, nullptr, nullptr, ((len + 1) & 0x7fff) <= 1 ? DATA_BINARY : DATA_FIXBINARY, len & 0x8000 ? DATA_NOT_NULL : 0, len & 0x7fff, true, phy_pos, v_added, v_dropped); dict_index_add_col(ind, table, table->get_col(i), 0, true); } // 加上trx_id和roll_ptr的列 dict_table_add_system_columns(table, table->heap); / Identify DB_TRX_ID and DB_ROLL_PTR in the index. */ // index中字段的順序和物理記錄保持一致 // 如果是聚簇索引棚赔,trx_id和roll_ptr放在主鍵的后面 if (is_versioned || (n_uniq != n)) { size_t i = 0; i = DATA_TRX_ID - 1 + n_uniq; ind->fields[i].col = &table->cols[n + DATA_TRX_ID]; ind->fields[i].col->set_phy_pos(table->cols[i].get_phy_pos()); i = DATA_ROLL_PTR - 1 + n_uniq; ind->fields[i].col = &table->cols[n + DATA_ROLL_PTR]; ind->fields[i].col->set_phy_pos(table->cols[i].get_phy_pos()); } return ptr;}</pre>

page_cur_parse_insert_rec

由于 MLOG_REC_INSERT 類型的 redo log 里做了壓縮帝簇,只記錄了和上一條記錄不一樣的部分,所以需要先解析出上一條記錄在頁面中的偏移量靠益、待插入記錄和上一條記錄第一個不相同的字節(jié) mismatch_index 和待插入記錄從 mismatch_index 開始的記錄長度 eng_seg_len丧肴,然后定位到上一條記錄,取出前 mismatch_index 個字節(jié)胧后,并從 redo log 中解析出待插入記錄從 mismatch_index 開始的部分芋浮,那么待插入記錄就是兩部分拼起來,最后插入到 B+樹中壳快。

<pre code-lang="bash" class="juejin-editor-highlight" style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 0.8em; position: relative; padding: 0.5em 1em; background: rgb(248, 248, 248); overflow: auto; border-radius: 2px;">// storage/innobase/page/page0cur.ccbyte page_cur_parse_insert_rec( bool is_short, /!< in: true if short inserts */ const byte ptr, /!< in: buffer */ const byte end_ptr, /!< in: buffer end */ buf_block_t block, /!< in: page or NULL */ dict_index_t index, /!< in: record descriptor */ mtr_t mtr) /!< in: mtr or NULL /{ ulint origin_offset = 0; / remove warning / ulint end_seg_len; ulint mismatch_index = 0; / remove warning */ page_t *page; rec_t *cursor_rec{nullptr}; byte buf1[1024]; // buf描述待插入記錄 byte *buf; const byte ptr2 = ptr; ulint info_and_status_bits = 0; / remove warning */ page_cur_t cursor; mem_heap_t *heap = nullptr; ulint offsets_[REC_OFFS_NORMAL_SIZE]; // offsets描述每個字段在物理記錄中的偏移量 ulint *offsets = offsets_; // offsets[0]存offsets數(shù)組占用的內(nèi)存大小 rec_offs_init(offsets_); page = block ? buf_block_get_frame(block) : nullptr; ulint offset; // 前一條記錄在頁面中的偏移量 offset = mach_read_from_2(ptr); ptr += 2; if (page != nullptr) cursor_rec = page + offset; // 該redo log對應(yīng)的記錄和前一條記錄不一樣的部分的長度纸巷,最低位是一個標(biāo)志位 end_seg_len = mach_parse_compressed(&ptr, end_ptr); info_and_status_bits = mach_read_from_1(ptr); ptr++; // 該redo log對應(yīng)的record header的長度 origin_offset = mach_parse_compressed(&ptr, end_ptr); // 和前一個記錄相比第一個不一樣的位置 mismatch_index = mach_parse_compressed(&ptr, end_ptr); if (!block) { return (const_cast<byte *>(ptr + (end_seg_len >> 1))); } ... // end_seg_len的最低位是一個標(biāo)志位镇草,所以真實(shí)的大小還需要除以2 end_seg_len >>= 1; // 如果buf在棧上分配的內(nèi)存不夠,就從堆上分配進(jìn)行擴(kuò)容 if (mismatch_index + end_seg_len < sizeof buf1) { buf = buf1; } else { buf = static_cast<byte *>(ut::malloc_withkey(UT_NEW_THIS_FILE_PSI_KEY, mismatch_index + end_seg_len)); } // 待插入記錄 = 前一條記錄的前mismatch_index個字節(jié) + 從ptr開始的eng_seg_len個字節(jié) if (mismatch_index) { ut_memcpy(buf, rec_get_start(cursor_rec, offsets), mismatch_index); } ut_memcpy(buf + mismatch_index, ptr, end_seg_len); ... // 將cursor定位到前一條記錄的位置 page_cur_position(cursor_rec, block, &cursor); // 構(gòu)建offsets數(shù)組何暇,用于描述每個字段在記錄中的偏移量 offsets = rec_get_offsets(buf + origin_offset, index, offsets, ULINT_UNDEFINED, UT_LOCATION_HERE, &heap); // 插入到B+樹中 page_cur_rec_insert(&cursor, buf + origin_offset, index, offsets, mtr); if (buf != buf1) { ut::free(buf); } if (UNIV_LIKELY_NULL(heap)) { mem_heap_free(heap); } return (const_cast<byte *>(ptr + end_seg_len));}</pre>

總結(jié)

這篇文章我們介紹了 redo log 的分類陶夜,不同種類的 redo log 的結(jié)構(gòu),并且分析了 redo log 在恢復(fù)時(shí)的流程相關(guān)的源碼裆站,歡迎大家關(guān)注StoneDB的開源代碼条辟。

StoneDB 介紹

StoneDB 是石原子科技自主設(shè)計(jì)研發(fā)的國內(nèi)首款完全兼容于 MySQL 生態(tài)的開源 一體化實(shí)時(shí) HTAP 數(shù)據(jù)庫產(chǎn)品,具備行列混存宏胯、智能索引等核心特性羽嫡,為 MySQL 數(shù)據(jù)庫提供在線數(shù)據(jù)實(shí)時(shí)就近分析服務(wù),能夠高效解決 MySQL 數(shù)據(jù)庫在分析型應(yīng)用場景中面臨的能力問題肩袍。同時(shí)杭棵,StoneDB 使用多存儲引擎架構(gòu)的設(shè)計(jì),事務(wù)引擎具有數(shù)據(jù)強(qiáng)一致特性氛赐,具備完整的事務(wù)并發(fā)處理能力魂爪,使得 StoneDB 可以替代 MySQL 數(shù)據(jù)庫滿足在線事務(wù)處理場景的需求,使用 MySQL 的用戶艰管,通過 StoneDB 可以實(shí)現(xiàn) TP+AP 混合負(fù)載滓侍,分析性能提升 10 倍以上顯著提升,不需要進(jìn)行數(shù)據(jù)遷移牲芋,也無需與其他 AP 集成撩笆,彌補(bǔ) MySQL 分析領(lǐng)域的空白。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缸浦,一起剝皮案震驚了整個濱河市夕冲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裂逐,老刑警劉巖歹鱼,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異絮姆,居然都是意外死亡醉冤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門篙悯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蚁阳,“玉大人,你說我怎么就攤上這事鸽照÷菥瑁” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長定血。 經(jīng)常有香客問我赔癌,道長,這世上最難降的妖魔是什么澜沟? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任灾票,我火速辦了婚禮,結(jié)果婚禮上茫虽,老公的妹妹穿的比我還像新娘刊苍。我一直安慰自己,他們只是感情好濒析,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布正什。 她就那樣靜靜地躺著,像睡著了一般号杏。 火紅的嫁衣襯著肌膚如雪婴氮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天盾致,我揣著相機(jī)與錄音主经,去河邊找鬼。 笑死庭惜,一個胖子當(dāng)著我的面吹牛旨怠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜈块,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼迷扇!你這毒婦竟也來了百揭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蜓席,失蹤者是張志新(化名)和其女友劉穎器一,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厨内,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡祈秕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雏胃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片请毛。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖瞭亮,靈堂內(nèi)的尸體忽然破棺而出方仿,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布仙蚜,位于F島的核電站此洲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏委粉。R本人自食惡果不足惜呜师,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贾节。 院中可真熱鬧汁汗,春花似錦、人聲如沸氮双。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戴差。三九已至送爸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暖释,已是汗流浹背袭厂。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留球匕,地道東北人纹磺。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像亮曹,于是被迫代替她去往敵國和親橄杨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容