《《MySQL運維內參》節選》要點:
本文介紹了《MySQL運維內參》節選,希望對您有用。如果有疑問,可以聯系我們。
本文接上文,開始接受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即可.關于這個問題,可以用下面的圖來簡單表示.
上面所說的基本上就是全部的日志刷盤時機了,相關內容都已經介紹清楚,在接下來的兩小節中,會重要講述數據庫的恢復問題.
前面已經很全面地介紹了日志的生成、格式、刷盤、工作原理等,但這些實際上只是數據庫運行時的一個“累贅”,沒辦法才會這樣做,因為如果數據庫不掛,日志是沒有用的,但不掛是不可能的,所以日志是必須的.而前面介紹的所有內容都是建立在有日志的前提下,解決如何提高性能,如何保證數據完整性等問題的.那這里將介紹關于日志的新內容,日志的用途之一:數據庫恢復.
在其他章節中,已經介紹了在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的執行過程.歸納起來,其所做的操作包括以下兩部分.
現在,主要的工作就落在了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
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/4180.html