1. 程式人生 > >InnoDB日誌管理機制(六) – 運維派

InnoDB日誌管理機制(六) – 運維派

本文接上文,開始接受MySQL的日誌刷盤和掃描問題,在本文中涉及到很多程式碼片段解析,程式碼註釋是關鍵,建議收藏本文並在電腦上閱讀。

日誌刷盤時機

前面已經介紹了大部分關於REDO日誌的內容了,但還有一個問題沒有講,就是日誌刷盤的時機,也就是什麼時候才會將日誌刷入磁碟。

現在已經知道,當MTR提交時,所產生的日誌,都會先寫入到Log
Buffer中,這是日誌產生的最初來源。從這個源頭開始,InnoDB會在不同的時機,將這些日誌寫入到磁碟,分別有下面5種時機。

1.Log Buffer空間用完了,便會將已經產生的Log
Buffer中的日誌刷到磁碟中,這個時機在前面介紹Mtr時已經說過了。這是最普遍的一種方式。

2.Master執行緒在後臺每秒鐘刷一次,將當前Log Buffer中的日誌刷到磁碟中。

3.每次執行DML操作時,都會主動檢查日誌空間是否足夠,如果使用空間的量已經超過了一個預設的經驗值,就會主動去刷日誌,以保證在後面真正執行時,不會在執行過程中被動地等待刷盤,但這裡只會是寫檔案(寫入OS快取中),不會刷磁碟。

4.在做檢查點的時候,要保證所有要刷的資料頁面中LSN值最小(最舊)的日誌已經刷入到磁碟。不然,如果此時資料庫掛了,日誌不存在,但資料頁面已經被修改,從而導致資料不一致,就違背了先寫日誌的原則。

5.提交邏輯事務時,會因為引數innodb_flush_log_at_trx_commit值的不同,產生不同的行為。如果設定為0,則在事務提交時,根本不會去刷日誌緩衝區,這種設定是最危險的,如果此時運氣不好,那對資料庫最新的修改都會丟失,即使事務已經提交了,但丟失的事務一般是最新1秒內產生的,因為Master執行緒會每隔1秒刷一次。如果設定為1,則在事務提交時會將日誌緩衝區中的日誌寫入到檔案中,同時會將這次寫入強刷到磁碟中,保證資料完全不丟失,但這種設定會使得資料庫效能下降很多,影響效能。如果設定為2,則在事務提交時會將日誌寫入到檔案中,但不會去刷盤,只要作業系統不掛,即使資料庫掛了,資料還是不會丟失,一般都是設定為2即可。關於這個問題,可以用下面的圖來簡單表示。資料庫

上面所說的基本上就是全部的日誌刷盤時機了,相關內容都已經介紹清楚,在接下來的兩小節中,會重要講述資料庫的恢復問題。

日誌刷盤

REDO日誌恢復

前面已經很全面地介紹了日誌的生成、格式、刷盤、工作原理等,但這些實際上只是資料庫執行時的一個“累贅”,沒辦法才會這樣做,因為如果資料庫不掛,日誌是沒有用的,但不掛是不可能的,所以日誌是必須的。而前面介紹的所有內容都是建立在有日誌的前提下,解決如何提高效能,如何保證資料完整性等問題的。那這裡將介紹關於日誌的新內容,日誌的用途之一:資料庫恢復。

在其他章節中,已經介紹了在InnoDB儲存引擎的啟動過程中,InnoDB需要做的事情有哪些,具體細節可以參考相關章節瞭解。在這一節中,需要重點關注的主要有兩個,包括recv_recovery_from_checkpoint_start及recv_recovery_from_checkpoint_finish兩個函式的處理(關於兩個函式的關係,請參閱InnoDB啟動相關章節)。

InnoDB啟動之前,肯定是處於shutdown狀態的,而導致shutdown的原因只有兩種可能性,即正常關閉及Crash關閉。這裡所說的資料恢復,主要處理的就是針對異常關閉時的情況。當然了,有一個叫innodb_fast_shutdown的引數,如果設定為2,也相當於是一次Crash了,道理也是一樣的。

那可能有人就要問了,如果正常關閉(innodb_fast_shutdown設定為0或者1),那是不是就不執行資料庫恢復了?其實不是這樣的,不管如何關閉資料庫,啟動時都會做資料庫恢復的操作,只不過正常關閉的情況下,不存在沒有做過checkpoint的日誌,或者說,最新的checkpoint已經在最新的LSN位置了,又或者說所有的資料頁面都已經被刷成了最新的狀態。說法可以有多種,但意義其實是一樣的。

日誌掃描

在開始準備做資料庫恢復時,首先要做的就是從日誌檔案中找到最新的檢查點資訊。我們已經知道,在日誌檔案最開始的4個頁面(每個頁面512位元組)中,儲存的是用來管理日誌檔案及日誌寫入情況的資訊,具體格式可以從前面看到。這裡所關注的檢查點資訊是儲存在第1號頁面和第3號頁面中的,即所謂的LOG_CHECKPOINT_1和LOG_CHECKPOINT_2。在做檢查點時,這兩個儲存位置是輪換著使用的。

基於此,想要找到最新的檢查點位置,就需要從上面的兩個位置中找到一個最大值,也就是在這個點之前所有的日誌都是失效的,並且對應的資料頁面都是完整的。而在這個位置之後的頁面,有可能是完整的,也有可能需要做REDO,這個決定於當時Buffer
Pool的刷盤情況,如果正好有被淘汰出去的頁面,那就是完整的,否則還需要通過REDO日誌來恢復。

先來看一下對應的精簡之後的程式碼,如下。

UNIV_INTERN dberr_t
recv_recovery_from_checkpoint_start_func(
lsn_t min_flushed_lsn,/*!< in: min flushed lsn from data files */
lsn_t max_flushed_lsn)/*!< in: max flushed lsn from data files */
{
/* loval variables … */

if (srv_force_recovery >= SRV_FORCE_NO_LOG_REDO) {
ib_logf(IB_LOG_LEVEL_INFO,
“The user has set SRV_FORCE_NO_LOG_REDO on, “
“skipping log redo”);
return(DB_SUCCESS);
}

recv_recovery_on = TRUE;

mutex_enter(&(log_sys->mutex));

/* Look for the latest checkpoint from any of the log groups */

/* 如上所述,這裡的工作就是用來從兩個Checkpoint的位置,找到最新的
max_cp_group中儲存的Checkpoint對應的資訊,包括最新LSN資訊、LSN對應的
日誌檔案中位置資訊等。前面已經知道,5.6版本之後的InnoDB都支援
總空間超過4G大小的日誌檔案,所以這個位置資訊包括了低32位值和高32
位值。max_cp_field用來表示最新位置是LOG_CHECKPOINT_1還是LOG_CHECKPOINT_1*/
err = recv_find_max_checkpoint(&max_cp_group, &max_cp_field);
if (err != DB_SUCCESS) {
mutex_exit(&(log_sys->mutex));
return(err);
}

/* 根據前面找到的max_cp_field資訊,把這個位置對應的檢查點資訊全部讀取出來,並
儲存到log_sys->checkpoint_buf空間中,下面會用到這部分資料 */
log_group_read_checkpoint_info(max_cp_group, max_cp_field);
buf = log_sys->checkpoint_buf;

/* 從上面的log_sys->checkpoint_buf中拿到最新的檢查點對應的LSN值及checkpoint_no值。
checkpoint_no就是在InnoDB做檢查點時,給每一次分配的一個編號,順序增長,值越大,
表示這個檢查點越是最近做的 */
checkpoint_lsn = mach_read_from_8(buf + LOG_CHECKPOINT_LSN);
checkpoint_no = mach_read_from_8(buf + LOG_CHECKPOINT_NO);

/* Read the first log file header to print a note if this is
a recovery from a restored InnoDB Hot Backup */

/* 讀出日誌頭的前4個頁面(一個頁面512位元組)*/
fil_io(OS_FILE_READ | OS_FILE_LOG, true, max_cp_group->space_id, 0,
0, 0, LOG_FILE_HDR_SIZE,
log_hdr_buf, max_cp_group);

/* 從上面讀取出的資訊中,找到儲存了ib_logfile的檔案管理中,每一個塊兒大小的
的位置。什麼?檔案塊兒大小可以改變?是的,在MySQL官方版本中,塊兒大小是不可以
修改的,都是512位元組,但Percona為了適應儲存裝置方面的科技進步,就支援了這個功能。
當然,支援是支援了,但不用也沒關係,如果不用,那麼這個位置的值就是0,就認為還是預設值512位元組*/

/* 宣告:
不過需要注意的是,這裡是為了說明一下這個特性在Percona中已經得到了支援。在前面
章節中,之所以在說明日誌檔案格式時沒有講到這個值,是因為在前面講到的內容中,在
LOG_FILE_WAS_CREATED_BY_HOT_BACKUP之後,就沒有其他內容了,這個頁面就是空的了。
而Percona是將塊兒大小的資訊追加到這個資訊之後,做到了與官方MySQL的相容 */
log_hdr_log_block_size = mach_read_from_4(log_hdr_buf + LOG_FILE_OS_FILE_LOG_BLOCK_SIZE);
if (log_hdr_log_block_size == 0) {
/* 0 means default value */
log_hdr_log_block_size = 512;
}

/* Percona在這裡很親切地問候你,如果日誌檔案中儲存的塊兒大小和
當前系統設定的值不一樣,也就是說這次資料庫啟動時修改了這個引數,
那麼它會告訴你,並且會給出友好的建議,可以RECREATE日誌檔案,很貼心 */
if (UNIV_UNLIKELY(log_hdr_log_block_size != srv_log_block_size)) {
fprintf(stderr,
“InnoDB: Error: The block size of ib_logfile (” ULINTPF
“) is not equal to innodb_log_block_size.\n”
“InnoDB: Error: Suggestion – Recreate log files.\n”,
log_hdr_log_block_size);
return(DB_ERROR);
}

/* Start reading the log groups from the checkpoint lsn up. The
variable contiguous_lsn contains an lsn up to which the log is
known to be contiguously written to all log groups. */

/* 到此為止,用來做恢復的資訊,都已經獲取到了:
checkpoint_lsn:表示的是從這個位置開始,後面的日誌需要做APPLY操作 */
recv_sys->parse_start_lsn = checkpoint_lsn;
recv_sys->scanned_lsn = checkpoint_lsn;
recv_sys->scanned_checkpoint_no = 0;
recv_sys->recovered_lsn = checkpoint_lsn;
srv_start_lsn = checkpoint_lsn;

/* 因為檔案讀取需要對齊到塊兒大小,所以recv_sys->scanned_lsn
會做對齊處理,contiguous_lsn表示的就是對齊之後的值 */
contiguous_lsn = ut_uint64_align_down(recv_sys->scanned_lsn, OS_FILE_LOG_BLOCK_SIZE);

/* 目前,InnoDB只支援一個GROUP,所以這裡的遍歷實際上沒有什麼意義,
這裡的處理是最重要的,所做的工作就是從contiguous_lsn的位置開始
掃描所有的日誌資料,然後進一步做分析、恢復等操作 */
group = UT_LIST_GET_FIRST(log_sys->log_groups);
while (group) {
recv_group_scan_log_recs(group, &contiguous_lsn, &group_scanned_lsn);
group->scanned_lsn = group_scanned_lsn;
group = UT_LIST_GET_NEXT(log_groups, group);
}

/* other codes … */
/* 做完資料庫恢復之後,要處理一下收尾工作。這個收尾工作非常重要,
類似於一個工程,在工作實施完成之後,還有一步是最後驗收,驗收的
時候一般會打上一個驗收合格的標誌,那麼這裡的操作也是同樣的道理,
具體的操作就是再做一次檢查點,更新一下最新的檢查點資訊,這樣之前
處理的所有REDO日誌就失效了,如果資料庫再掛了,那也是重新洗牌,與
這次就沒有什麼關係了 */
recv_synchronize_groups();
/* The database is now ready to start almost normal processing of user
transactions: transaction rollbacks and the application of the log
records in the hash table can be run in background. */

return(DB_SUCCESS);
}

上面的程式碼,其實就是我們所熟悉的函式recv_recovery_from_checkpoint_start_func的執行過程。歸納起來,其所做的操作包括以下兩部分。

  1. 從日誌檔案的固定位置找到最新的檢查點資訊。
  2. 從最新的檢查點位置開始掃描日誌檔案,做資料庫恢復。

現在,主要的工作就落在了recv_group_scan_log_recs上面,這個函式所要做的工作,就是將checkpoint_lsn位置開始的日誌分片處理,每一片為2M大小,對應的精簡之後的程式碼如下。

static void recv_group_scan_log_recs(
log_group_t* group,
lsn_t* contiguous_lsn,
lsn_t* group_scanned_lsn
)
{

/* local variables … */
finished = FALSE;
start_lsn = *contiguous_lsn;

/* 等待分析完畢 */
while (!finished) {

/* RECV_SCAN_SIZE大小為4*16K,也就是分片大小為64K,
因為已經知道,InnoDB的日誌LSN的增長和資料量寫入的增長是同步的。
也就是說LSN加1,表示日誌就多寫入一個位元組,所以這裡在LSN的計算中,加上
64K,表示的就是2M的日誌量 */
end_lsn = start_lsn + RECV_SCAN_SIZE;

/* 在下面這個函式中,會根據之前讀出來的LSN所對應的日誌檔案偏移位置,
將2M內容讀取出來,存到log_sys->buf中,以待後面分析 */
log_group_read_log_seg(LOG_RECOVER, log_sys->buf, group, start_lsn, end_lsn, FALSE);

/* recv_scan_log_recs中,會檢查到日誌已經分析完畢,那
資料庫的REDO就算基本完成了,上面的while迴圈停止,具體如何判斷日誌
內容讀取完畢,請待進一步的講述 */

finished = recv_scan_log_recs(
(buf_pool_get_n_pages()
– (recv_n_pool_free_frames * srv_buf_pool_instances))
* UNIV_PAGE_SIZE,
TRUE, log_sys->buf, RECV_SCAN_SIZE,
start_lsn, contiguous_lsn, group_scanned_lsn);

/* 下一個分片,從上一個分片的結束位置開始 */
start_lsn = end_lsn;
}
}

從上面的函式可以看到,資料庫恢復時會根據最新檢查點的位置,將日誌不斷分片讀取,然後進行分片處理,這裡再來分析一下InnoDB是如何做分片處理的。繼續看精簡之後的程式碼,如下。

UNIV_INTERN
ibool
recv_scan_log_recs(
ulint available_memory,
ibool store_to_hash,
const byte* buf, /*!< in: buffer containing a log segment or garbage */
ulint len, /*!< in: buffer length */
lsn_t start_lsn, /*!< in: buffer start lsn */
lsn_t* contiguous_lsn,
lsn_t* group_scanned_lsn)
{
/* local variables … */
/* 通過finished來表示恢復過程是否已經做完,如果做完則返回值為true */
finished = FALSE;
/* 儲存了64KB的日誌 */
log_block = buf;
scanned_lsn = start_lsn;
more_data = FALSE;

do {
/* 讀出當前塊中儲存的資料量,一個塊兒,預設大小為512位元組,
如果沒有掃描到最後一塊,這個大小就都是512,因為日誌都是連續儲存的 */
data_len = log_block_get_data_len(log_block);

scanned_lsn += data_len;

/* 如果當前塊兒中的資料量大於0,就會處理當前塊 */
if (scanned_lsn > recv_sys->scanned_lsn) {
/* recv_sys,用來儲存分析之後的日誌。這裡的工作是將從日誌
檔案中讀取出來的原始資料去掉頭(12位元組)尾(4位元組)資料之後,
將中間真正的日誌取出來,放到recv_sys所指的快取空間中,這部分資料
才是REDO恢復真正需要的資料,而在日誌檔案中儲存的原始日誌(包括頭尾)
是為了更好更方便地管理而設定的,所以在這裡會有這麼一個轉換的步驟。*/

/* 如果recv_sys的快取空間已快要超過分析緩衝區大小(RECV_PARSING_BUF_SIZE=2M),
則說明當前recv_sys中快取的日誌太多,並且這些日誌還不能滿足APPLY的條件。
此時說明日誌儲存出現了錯誤,會在errlog中報出下面的資訊,表示Recovery可能失敗了。
為什麼是RECV_PARSING_BUF_SIZE的大小呢?因為InnoDB認為,在寫日誌時,不會有
MTR所寫的日誌量超過這個值,如果有,則只能是日誌儲存或者解析出了問題。*/
if (recv_sys->len + 4 * OS_FILE_LOG_BLOCK_SIZE >= RECV_PARSING_BUF_SIZE) {
fprintf(stderr, “InnoDB: Error: log parsing”
” buffer overflow.”
” Recovery may have failed!\n”);
recv_sys->found_corrupt_log = TRUE;
} else if (!recv_sys->found_corrupt_log) {
/* 這裡就是將當前塊中真正的日誌內容拿出來,儲存到recv_sys快取中去*/
more_data = recv_sys_add_to_parsing_buf(log_block, scanned_lsn);
}

/* 更新scanned_lsn,表示已經掃描的LSN值已經到了這個位置 */
recv_sys->scanned_lsn = scanned_lsn;
recv_sys->scanned_checkpoint_no = log_block_get_checkpoint_no(log_block);
}

/* 從這裡也可以印證上面所述,如果一個日誌塊不足OS_FILE_LOG_BLOCK_SIZE(預設512位元組),
則說明整個REDO日誌掃描已經結束,已經掃描到了日誌結尾的位置 */
if (data_len < OS_FILE_LOG_BLOCK_SIZE) {
/* Log data for this group ends here */
finished = TRUE;
break;
} else {
/* 沒有結束則向前掃描OS_FILE_LOG_BLOCK_SIZE(512位元組)的偏移量*/
log_block += OS_FILE_LOG_BLOCK_SIZE;
}
} while (log_block < buf + len && !finished);

*group_scanned_lsn = scanned_lsn;
/* 上面已經將當前塊或之前塊的日誌放入到了recv_sys的緩衝區中了,
下面就會對這部分日誌做一次處理,呼叫的核心函式為recv_parse_log_recs,
這個函式所要做的工作,接下來會以程式碼講解的方式詳細講述 */
if (more_data && !recv_sys->found_corrupt_log) {
/* Try to parse more log records */
recv_parse_log_recs(store_to_hash);

/* 從這裡看到,recv_parse_log_recs將日誌進一步處理之後,如果佔用的
快取空間大於available_memory,就需要APPLY了,而這個快取空間就是用於
恢復HASH表,這個HASH表後面會講述。available_memory的大小,與Buffer Pool
有關係,InnoDB會拿一部分Buffer Pool空間來做REDO日誌的恢復。
下面這個函式recv_apply_hashed_log_recs,
也會在後面說到 */
if (store_to_hash && mem_heap_get_size(recv_sys->heap) > available_memory) {
recv_apply_hashed_log_recs(FALSE);
}

/* 在recv_parse_log_recs中,處理掉一部分日誌之後,緩衝區中
一般會有剩餘的不完整的日誌,這部分日誌還不能被處理,需要等待讀取
更多的日誌進來,拼接之後才能繼續處理,那麼這裡就需要將剩餘的
這部分日誌移到緩衝區最開始的位置,以便繼續拼接更多的日誌內容 */
if (recv_sys->recovered_offset > RECV_PARSING_BUF_SIZE / 4) {
/* Move parsing buffer data to the buffer start */
recv_sys_justify_left_parsing_buf();
}
}

return(finished);
}

日誌

從上面的程式碼中,可以知道,InnoDB為了更好地管理日誌檔案,將連續的日誌內容以塊為單位來儲存,加上頭尾資訊,繼續連續儲存,而在使用它的時候,又將這些日誌以塊為單位讀取進來,掐頭去尾,拼接在一起,進一步做分析處理。下面就看一下recv_parse_log_recs是如何做日誌分析的。

static ibool recv_parse_log_recs(
ibool store_to_hash
)
{
/* local variables … */
/* 一個大的迴圈,連續處理恢復緩衝區中的日誌內容,
直到處理完,或者剩下的不是一個完整的MTR為止 */
loop:
/* 當前日誌緩衝區中,日誌的開始位置 */
ptr = recv_sys->buf + recv_sys->recovered_offset;
/* 當前日誌緩衝區中,日誌的結束位置 */
end_ptr = recv_sys->buf + recv_sys->len;
if (ptr == end_ptr) {
return(FALSE);
}

/* MLOG_SINGLE_REC_FLAG表示的是,當前日誌所對應的MTR,只寫了這一條日誌,
所以這裡就作為特殊情況特別處理了。一般情況下,初始化一個頁面,或者建立
一個頁面等,屬於這種情況,在寫日誌的時候,會在日誌頭中加上這個標誌 */
single_rec = (ulint)*ptr & MLOG_SINGLE_REC_FLAG;
if (single_rec || *ptr == MLOG_DUMMY_RECORD) {
/* The mtr only modified a single page, or this is a file op */
old_lsn = recv_sys->recovered_lsn;

/* 如註釋所述:Try to parse a log record, fetching its type, space id,
page no, and a pointer to the body of the log record */
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);

/* 更新進度 */
recv_sys->recovered_offset += len;
recv_sys->recovered_lsn = new_recovered_lsn;

if (type == MLOG_DUMMY_RECORD) {
/* Do nothing */
} else if (!store_to_hash) {
/* In debug checking, update a replicate page
according to the log record, and check that it
becomes identical with the original page */
} else if (type == MLOG_FILE_CREATE || type == MLOG_FILE_CREATE2
|| type == MLOG_FILE_RENAME || type == MLOG_FILE_DELETE) {
/* In normal mysqld crash recovery we do not try to
replay file operations */
} else {
/* 將分析出來的日誌資訊存到一個HASH表中,又是一層快取,
這是第三層。後面可以瞭解HASH表的管理方法 */
recv_add_to_hash_table(type, space, page_no, body, ptr + len, old_lsn,
recv_sys->recovered_lsn);
}
} else {
/* 與上面相反的是,這裡表示的是,一個MTR,
包括多個日誌記錄,所以這裡需要一個個地去分析處理 */
total_len = 0;
n_recs = 0;

/* 這裡很關鍵,在前面介紹的日誌記錄型別中,已經提到過關於
MLOG_MULTI_REC_END型別的作用,它用來標誌一個MTR是不是結束
了。如果找到了這麼一條日誌,則說明前面的日誌是完整的,那這個MTR
就是可以做APPLY的。而MTR,為何被稱為mini-transaction,也正是因為
事務所具備的特性是原子性,要麼全做,要麼全不做,只有找到了
這個標誌,才說明這個MTR(物理事務)是完整的,這部分日誌才可以被
APPLY。可能有人會問,這個標誌有沒有可能找不到?答案是有可能。
如果真地找不到,這個日誌就不正常,說明這個MTR後面一部分日誌
沒有被完整地寫入日誌檔案,那這個邏輯事務必定未提交或未提交成功
(如果提交,則與引數innodb_flush_log_at_trx_commit有關),這個MTR
就被忽略了。不過可以肯定的是,這個MTR也是本次資料庫啟動時,涉及
日誌內容中的最後一個MTR了(除非日誌檔案內容儲存或者解析出錯了)*/
for (;;) {
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);
/* 沒有完整內容了,則返回,不會繼續處理了 */
if (len == 0 || recv_sys->found_corrupt_log) {
if (recv_sys->found_corrupt_log) {
recv_report_corrupt_log( ptr, type, space, page_no);
}
return(FALSE);
}
total_len += len;
n_recs++;
ptr += len;
if (type == MLOG_MULTI_REC_END) {
/* Found the end mark for the records */
break;
}
}

/* 能到這裡,說明上面已經找到了MTR的結束標誌,說明這個MTR是完整的,這樣
就會重新處理這部分日誌。啊?重新處理?是的,將上面檢查過的重新掃描一遍。
不過這次就可以自信滿滿地去處理每一個日誌記錄了,而不需要擔心日誌的
原子性問題了 */

/* 不過,這裡的程式碼是不是可以做一些優化?對於每一個
MTR,都要掃描兩遍?這樣感覺會對效能造成不小的影響。
至於如何優化,方法總是有的,事在人為,關鍵是對於那些將Log檔案設定得
很大,並且經常出現異常掛機的使用者來說,他們有沒有對效能的需求。方法總是
跟著需求走的,有了需求,問題自然可以解決。 */
/* Add all the records to the hash table */
ptr = recv_sys->buf + recv_sys->recovered_offset;
for (;;) {
old_lsn = recv_sys->recovered_lsn;
/* 繼續分析日誌記錄,找到型別、表空間ID、頁面號及日誌內容 */
len = recv_parse_log_rec(ptr, end_ptr, &type, &space, &page_no, &body);

/* 更新進度 */
recv_sys->recovered_offset += len;
recv_sys->recovered_lsn = recv_calc_lsn_on_data_add(old_lsn, len);
/* 又見MLOG_MULTI_REC_END,說明已經處理完了這個MTR,則需要繼續處理下一個
MTR。結束之後,做一次大迴圈,直接goto loop,從頭再來。*/
if (type == MLOG_MULTI_REC_END) {
/* Found the end mark for the records */
break;
}

/* 將每一個分析出來的日誌記錄,加入到HASH表中。如此看來,這個HASH
表的管理,就是下一步要研究清楚的內容了。 */
if (store_to_hash) {
recv_add_to_hash_table(type, space, page_no, body, ptr + len,
old_lsn, new_recovered_lsn);
}

ptr += len;
}
}

/* 從頭再來,下一個MTR */
goto loop;
}

從上面的程式碼中可以看出來,InnoDB拿到連續的日誌內容之後,以一個mini-transaction(MTR,物理事務)所包含的日誌為單位做分析,再將一個MTR中所有的日誌記錄一個個地分開,儲存到HASH表中,以便做APPLY。那麼下面再來看加入到HASH表中的操作是如何做的。

static
void
recv_add_to_hash_table(
/*===================*/
byte type, /*!< in: log record type */
ulint space, /*!< in: space id */
ulint page_no, /*!< in: page number */
byte* body, /*!< in: log record body */
byte* rec_end, /*!< in: log record end */
lsn_t start_lsn, /*!< in: start lsn of the mtr */
lsn_t end_lsn) /*!< in: end lsn of the mtr */
{
recv_t* recv;
ulint len;
recv_data_t* recv_data;
recv_data_t** prev_field;
recv_addr_t* recv_addr;

len = rec_end – body;
/* 針對每一條日誌記錄,都會有一個recv_t的結構來儲存它,其包括的成員從下面可以看到 */
recv = static_cast<recv_t*>(mem_heap_alloc(recv_sys->heap, sizeof(recv_t)));

/* 成員賦值 */
recv->type = type;
recv->len = rec_end – body;
recv->start_lsn = start_lsn;
recv->end_lsn = end_lsn;
/* 這裡很重要,可以看到,InnoDB是根據space和page_no獲取一個recv_addr。
如果沒有recv_addr,就建立一個,被管理到recv_sys->addr_hash的HASH表中,這裡
出現了上面提到的HASH表,也就是說,這個HASH表的鍵值是space, page_no
的組合值,也就是所有日誌中對應的表空間頁面,都會有這樣一個快取物件 */
recv_addr = recv_get_fil_addr_struct(space, page_no);
if (recv_addr == NULL) {
recv_addr = static_cast<recv_addr_t*>(mem_heap_alloc(recv_sys->heap, sizeof(recv_addr_t)));
recv_addr->space = space;
recv_addr->page_no = page_no;
recv_addr->state = RECV_NOT_PROCESSED;
UT_LIST_INIT(recv_addr->rec_list);
HASH_INSERT(recv_addr_t, addr_hash, recv_sys->addr_hash,
recv_fold(space, page_no), recv_addr);
recv_sys->n_addrs++;
}

/* 將當前日誌記錄,放到與之對應的快取物件中,表示當前日誌所要恢復的位置
就是在space, page_no頁面中 */
UT_LIST_ADD_LAST(rec_list, recv_addr->rec_list, recv);

/* 儲存日誌內容時,會用到下面程式碼 */
prev_field = &(recv->data);

/* 如上面註釋所述,將日誌記錄的內容,即日誌體(body)
寫入到日誌記錄recv_t結構物件的data中*/
while (rec_end > body) {
len = rec_end – body;
if (len > RECV_DATA_BLOCK_SIZE) {
len = RECV_DATA_BLOCK_SIZE;
}
recv_data = static_cast<recv_data_t*>(mem_heap_alloc(recv_sys->heap, sizeof(recv_data_t) + len));
*prev_field = recv_data;
memcpy(recv_data + 1, body, len);
prev_field = &(recv_data->next);
body += len;
}

*prev_field = NULL;
}

上面這段程式碼讓我們明白,InnoDB將每一個日誌記錄分開之後,儲存到了以表空間ID及頁面號為鍵值的HASH表中。也就是說,相同的頁面肯定是儲存在一起的,並且在同一個頁面上的日誌是以先後順序掛在這個對應的HASH節點中的,從而保證了REDO操作的有序性。

從這些程式碼段中可以看到,快取到HASH表之後,應該是可以找合適的時機去APPLY了。那什麼時候才是合適的時機呢?返回去看到函式recv_scan_log_recs的最後呼叫了函式recv_apply_hashed_log_recs,那麼這就是真正做APPLY的函數了。下面詳細看一下它的實現。函式

(作者注:懂了原始碼,學習MySQL還算個球?)

文章來自微信公眾號:DBAce