庖丁解 InnoDB 之 Redolog
數(shù)據(jù)庫(kù)故障恢復(fù)機(jī)制的前世今生一文中提到,今生磁盤(pán)數(shù)據(jù)庫(kù)為了在保證數(shù)據(jù)庫(kù)的原子性(A, Atomic) 和持久性(D, Durability)的同時(shí),還能以靈活的刷盤(pán)策略來(lái)充分利用磁盤(pán)順序?qū)懙男阅?,?huì)記錄REDO和UNDO日志,即ARIES方法。本文將重點(diǎn)介紹REDO LOG的作用,記錄的內(nèi)容,組織結(jié)構(gòu),寫(xiě)入方式等內(nèi)容,希望讀者能夠更全面準(zhǔn)確的理解REDO LOG在InnoDB中的位置。本文基于MySQL 8.0代碼。
一、為什么需要記錄REDO
為了取得更好的讀寫(xiě)性能,InnoDB會(huì)將數(shù)據(jù)緩存在內(nèi)存中(InnoDB Buffer Pool),對(duì)磁盤(pán)數(shù)據(jù)的修改也會(huì)落后于內(nèi)存,這時(shí)如果進(jìn)程或機(jī)器崩潰,會(huì)導(dǎo)致內(nèi)存數(shù)據(jù)丟失,為了保證數(shù)據(jù)庫(kù)本身的一致性和持久性,InnoDB維護(hù)了REDO LOG。修改Page之前需要先將修改的內(nèi)容記錄到REDO中,并保證REDO LOG早于對(duì)應(yīng)的Page落盤(pán),也就是常說(shuō)的WAL,Write Ahead Log。當(dāng)故障發(fā)生導(dǎo)致內(nèi)存數(shù)據(jù)丟失后,InnoDB會(huì)在重啟時(shí),通過(guò)重放REDO,將Page恢復(fù)到崩潰前的狀態(tài)。
二、需要什么樣的REDO
那么我們需要什么樣的REDO呢?首先,REDO的維護(hù)增加了一份寫(xiě)盤(pán)數(shù)據(jù),同時(shí)為了保證數(shù)據(jù)正確,事務(wù)只有在他的REDO全部落盤(pán)才能返回用戶成功,REDO的寫(xiě)盤(pán)時(shí)間會(huì)直接影響系統(tǒng)吞吐,顯而易見(jiàn),REDO的數(shù)據(jù)量要盡量少。其次,系統(tǒng)崩潰總是發(fā)生在始料未及的時(shí)候,當(dāng)重啟重放REDO時(shí),系統(tǒng)并不知道哪些REDO對(duì)應(yīng)的Page已經(jīng)落盤(pán),因此REDO的重放必須可重入,即REDO操作要保證冪等。最后,為了便于通過(guò)并發(fā)重放的方式加快重啟恢復(fù)速度,REDO應(yīng)該是基于Page的,即一個(gè)REDO只涉及一個(gè)Page的修改。
熟悉的讀者會(huì)發(fā)現(xiàn),數(shù)據(jù)量小是Logical Logging的優(yōu)點(diǎn),而冪等以及基于Page正是Physical Logging的優(yōu)點(diǎn),因此InnoDB采取了一種稱為Physiological Logging的方式,來(lái)兼得二者的優(yōu)勢(shì)。所謂Physiological Logging,就是以Page為單位,但在Page內(nèi)以邏輯的方式記錄。舉個(gè)例子,MLOG_REC_UPDATE_IN_PLACE類型的REDO中記錄了對(duì)Page中一個(gè)Record的修改,方法如下:
- (Page ID,Record Offset,(Filed 1, Value 1) ... (Filed i, Value i) ... )
其中,PageID指定要操作的Page頁(yè),Record Offset記錄了Record在Page內(nèi)的偏移位置,后面的Field數(shù)組,記錄了需要修改的Field以及修改后的Value。
由于Physiological Logging的方式采用了物理Page中的邏輯記法,導(dǎo)致兩個(gè)問(wèn)題:
1、需要基于正確的Page狀態(tài)上重放REDO
由于在一個(gè)Page內(nèi),REDO是以邏輯的方式記錄了前后兩次的修改,因此重放REDO必須基于正確的Page狀態(tài)。然而InnoDB默認(rèn)的Page大小是16KB,是大于文件系統(tǒng)能保證原子的4KB大小的,因此可能出現(xiàn)Page內(nèi)容成功一半的情況。InnoDB中采用了Double Write Buffer的方式來(lái)通過(guò)寫(xiě)兩次的方式保證恢復(fù)的時(shí)候找到一個(gè)正確的Page狀態(tài)。這部分會(huì)在之后介紹Buffer Pool的時(shí)候詳細(xì)介紹。
2、需要保證REDO重放的冪等
Double Write Buffer能夠保證找到一個(gè)正確的Page狀態(tài),我們還需要知道這個(gè)狀態(tài)對(duì)應(yīng)REDO上的哪個(gè)記錄,來(lái)避免對(duì)Page的重復(fù)修改。為此,InnoDB給每個(gè)REDO記錄一個(gè)全局唯一遞增的標(biāo)號(hào)LSN(Log Sequence Number)。Page在修改時(shí),會(huì)將對(duì)應(yīng)的REDO記錄的LSN記錄在Page上(FIL_PAGE_LSN字段),這樣恢復(fù)重放REDO時(shí),就可以來(lái)判斷跳過(guò)已經(jīng)應(yīng)用的REDO,從而實(shí)現(xiàn)重放的冪等。
三、REDO中記錄了什么內(nèi)容
知道了InnoDB中記錄REDO的方式,那么REDO里具體會(huì)記錄哪些內(nèi)容呢?為了應(yīng)對(duì)InnoDB各種各樣不同的需求,到MySQL 8.0為止,已經(jīng)有多達(dá)65種的REDO記錄。用來(lái)記錄這不同的信息,恢復(fù)時(shí)需要判斷不同的REDO類型,來(lái)做對(duì)應(yīng)的解析。根據(jù)REDO記錄不同的作用對(duì)象,可以將這65中REDO劃分為三個(gè)大類:作用于Page,作用于Space以及提供額外信息的Logic類型。
1、作用于Page的REDO
這類REDO占所有REDO類型的絕大多數(shù),根據(jù)作用的Page的不同類型又可以細(xì)分為,Index Page REDO,Undo Page REDO,Rtree PageREDO等。比如MLOG_REC_INSERT,MLOG_REC_UPDATE_IN_PLACE,MLOG_REC_DELETE三種類型分別對(duì)應(yīng)于Page中記錄的插入,修改以及刪除。這里還是以MLOG_REC_UPDATE_IN_PLACE為例來(lái)看看其中具體的內(nèi)容:
其中,Type就是MLOG_REC_UPDATE_IN_PLACE類型,Space ID和Page Number唯一標(biāo)識(shí)一個(gè)Page頁(yè),這三項(xiàng)是所有REDO記錄都需要有的頭信息,后面的是MLOG_REC_UPDATE_IN_PLACE類型獨(dú)有的,其中Record Offset用給出要修改的記錄在Page中的位置偏移,Update Field Count說(shuō)明記錄里有幾個(gè)Field要修改,緊接著對(duì)每個(gè)Field給出了Field編號(hào)(Field Number),數(shù)據(jù)長(zhǎng)度(Field Data Length)以及數(shù)據(jù)(Filed Data)。
2、作用于Space的REDO
這類REDO針對(duì)一個(gè)Space文件的修改,如MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME分別對(duì)應(yīng)對(duì)一個(gè)Space的創(chuàng)建,刪除以及重命名。由于文件操作的REDO是在文件操作結(jié)束后才記錄的,因此在恢復(fù)的過(guò)程中看到這類日志時(shí),說(shuō)明文件操作已經(jīng)成功,因此在恢復(fù)過(guò)程中大多只是做對(duì)文件狀態(tài)的檢查,以MLOG_FILE_CREATE來(lái)看看其中記錄的內(nèi)容:
同樣的前三個(gè)字段還是Type,Space ID和Page Number,由于是針對(duì)Page的操作,這里的Page Number永遠(yuǎn)是0。在此之后記錄了創(chuàng)建的文件flag以及文件名,用作重啟恢復(fù)時(shí)的檢查。
3、提供額外信息的Logic REDO
除了上述類型外,還有少數(shù)的幾個(gè)REDO類型不涉及具體的數(shù)據(jù)修改,只是為了記錄一些需要的信息,比如最常見(jiàn)的MLOG_MULTI_REC_END就是為了標(biāo)識(shí)一個(gè)REDO組,也就是一個(gè)完整的原子操作的結(jié)束。
4、REDO是如何組織的
所謂REDO的組織方式,就是如何把需要的REDO內(nèi)容記錄到磁盤(pán)文件中,以方便高效的REDO寫(xiě)入,讀取,恢復(fù)以及清理。我們這里把REDO從上到下分為三層:邏輯REDO層、物理REDO層和文件層。
1) 邏輯REDO層
這一層是真正的REDO內(nèi)容,REDO由多個(gè)不同Type的多個(gè)REDO記錄收尾相連組成,有全局唯一的遞增的偏移sn,InnoDB會(huì)在全局log_sys中維護(hù)當(dāng)前sn的最大值,并在每次寫(xiě)入數(shù)據(jù)時(shí)將sn增加REDO內(nèi)容長(zhǎng)度。如下圖所示:
2 )物理REDO層
磁盤(pán)是塊設(shè)備,InnoDB中也用Block的概念來(lái)讀寫(xiě)數(shù)據(jù),一個(gè)Block的長(zhǎng)度OS_FILE_LOG_BLOCK_SIZE等于磁盤(pán)扇區(qū)的大小512B,每次IO讀寫(xiě)的最小單位都是一個(gè)Block。除了REDO數(shù)據(jù)以外,Block中還需要一些額外的信息,下圖所示一個(gè)Log Block的的組成,包括12字節(jié)的Block Header:前4字節(jié)中Flush Flag占用最高位bit,標(biāo)識(shí)一次IO的第一個(gè)Block,剩下的31個(gè)個(gè)bit是Block編號(hào);之后是2字節(jié)的數(shù)據(jù)長(zhǎng)度,取值在[12,508];緊接著2字節(jié)的First Record Offset用來(lái)指向Block中第一個(gè)REDO組的開(kāi)始,這個(gè)值的存在使得我們對(duì)任何一個(gè)Block都可以找到一個(gè)合法的的REDO開(kāi)始位置;最后的4字節(jié)Checkpoint Number記錄寫(xiě)B(tài)lock時(shí)的next_checkpoint_number,用來(lái)發(fā)現(xiàn)文件的循環(huán)使用,這個(gè)會(huì)在文件層詳細(xì)講解。Block末尾是4字節(jié)的Block Tailer,記錄當(dāng)前Block的Checksum,通過(guò)這個(gè)值,讀取Log時(shí)可以明確Block數(shù)據(jù)有沒(méi)有被完整寫(xiě)完。
Block中剩余的中間498個(gè)字節(jié)就是REDO真正內(nèi)容的存放位置,也就是我們上面說(shuō)的邏輯REDO。我們現(xiàn)在將邏輯REDO放到物理REDO空間中,由于Block內(nèi)的空間固定,而REDO長(zhǎng)度不定,因此可能一個(gè)Block中有多個(gè)REDO,也可能一個(gè)REDO被拆分到多個(gè)Block中,如下圖所示,棕色和紅色分別代表Block Header和Tailer,中間的REDO記錄由于前一個(gè)Block剩余空間不足,而被拆分在連續(xù)的兩個(gè)Block中。
由于增加了Block Header和Tailer的字節(jié)開(kāi)銷,在物理REDO空間中用LSN來(lái)標(biāo)識(shí)偏移,可以看出LSN和SN之間有簡(jiǎn)單的換算關(guān)系:
- constexpr inline lsn_t log_translate_sn_to_lsn(lsn_t sn) {
- return (sn / LOG_BLOCK_DATA_SIZE * OS_FILE_LOG_BLOCK_SIZE +
- sn % LOG_BLOCK_DATA_SIZE + LOG_BLOCK_HDR_SIZE);
- }
SN加上之前所有的Block的Header以及Tailer的長(zhǎng)度就可以換算到對(duì)應(yīng)的LSN,反之亦然。
3) 文件層
最終REDO會(huì)被寫(xiě)入到REDO日志文件中,以ib_logfile0、ib_logfile1...命名,為了避免創(chuàng)建文件及初始化空間帶來(lái)的開(kāi)銷,InooDB的REDO文件會(huì)循環(huán)使用,通過(guò)參數(shù)innodb_log_files_in_group可以指定REDO文件的個(gè)數(shù)。多個(gè)文件收尾相連順序?qū)懭隦EDO內(nèi)容。每個(gè)文件以Block為單位劃分,每個(gè)文件的開(kāi)頭固定預(yù)留4個(gè)Block來(lái)記錄一些額外的信息,其中第一個(gè)Block稱為Header Block,之后的3個(gè)Block在0號(hào)文件上用來(lái)存儲(chǔ)Checkpoint信息,而在其他文件上留空:
其中第一個(gè)Header Block的數(shù)據(jù)區(qū)域記錄了一些文件信息,如下圖所示,4字節(jié)的Formate字段記錄Log的版本,不同版本的LOG,會(huì)有REDO類型的增減,這個(gè)信息是8.0開(kāi)始才加入的;8字節(jié)的Start LSN標(biāo)識(shí)當(dāng)前文件開(kāi)始LSN,通過(guò)這個(gè)信息可以將文件的offset與對(duì)應(yīng)的lsn對(duì)應(yīng)起來(lái);最后是最長(zhǎng)32位的Creator信息,正常情況下會(huì)記錄MySQL的版本。
現(xiàn)在我們將REDO放到文件空間中,如下圖所示,邏輯REDO是真正需要的數(shù)據(jù),用sn索引,邏輯REDO按固定大小的Block組織,并添加Block的頭尾信息形成物理REDO,以lsn索引,這些Block又會(huì)放到循環(huán)使用的文件空間中的某一位置,文件中用offset索引:
雖然通過(guò)LSN可以唯一標(biāo)識(shí)一個(gè)REDO位置,但最終對(duì)REDO的讀寫(xiě)還需要轉(zhuǎn)換到對(duì)文件的讀寫(xiě)IO,這個(gè)時(shí)候就需要表示文件空間的offset,他們之間的換算方式如下:
- const auto real_offset =
- log.current_file_real_offset + (lsn - log.current_file_lsn);
切換文件時(shí)會(huì)在內(nèi)存中更新當(dāng)前文件開(kāi)頭的文件offset,current_file_real_offset,以及對(duì)應(yīng)的LSN,current_file_lsn,通過(guò)這兩個(gè)值可以方便地用上面的方式將LSN轉(zhuǎn)化為文件offset。注意這里的offset是相當(dāng)于整個(gè)REDO文件空間而言的,由于InnoDB中讀寫(xiě)文件的space層實(shí)現(xiàn)支持多個(gè)文件,因此,可以將首位相連的多個(gè)REDO文件看成一個(gè)大文件,那么這里的offset就是這個(gè)大文件中的偏移。
五、如何高效地寫(xiě)REDO
作為維護(hù)數(shù)據(jù)庫(kù)正確性的重要信息,REDO日志必須在事務(wù)提交前保證落盤(pán),否則一旦斷電將會(huì)有數(shù)據(jù)丟失的可能,因此從REDO生成到最終落盤(pán)的完整過(guò)程成為數(shù)據(jù)庫(kù)寫(xiě)入的關(guān)鍵路徑,其效率也直接決定了數(shù)據(jù)庫(kù)的寫(xiě)入性能。這個(gè)過(guò)程包括REDO內(nèi)容的產(chǎn)生,REDO寫(xiě)入InnoDB Log Buffer,從InnoDB Log Buffer寫(xiě)入操作系統(tǒng)Page Cache,以及REDO刷盤(pán),之后還需要喚醒等待的用戶線程完成Commit。下面就通過(guò)這幾個(gè)階段來(lái)看看InnoDB如何在高并發(fā)的情況下還能高效地完成寫(xiě)REDO。
1、REDO產(chǎn)生
我們知道事務(wù)在寫(xiě)入數(shù)據(jù)的時(shí)候會(huì)產(chǎn)生REDO,一次原子的操作可能會(huì)包含多條REDO記錄,這些REDO可能是訪問(wèn)同一Page的不同位置,也可能是訪問(wèn)不同的Page(如Btree節(jié)點(diǎn)分裂)。InnoDB有一套完整的機(jī)制來(lái)保證涉及一次原子操作的多條REDO記錄原子,即恢復(fù)的時(shí)候要么全部重放,要不全部不重放,這部分將在之后介紹恢復(fù)邏輯的時(shí)候詳細(xì)介紹,本文只涉及其中最基本的要求,就是這些REDO必須連續(xù)。InnoDB中通過(guò)min-transaction實(shí)現(xiàn),簡(jiǎn)稱mtr,需要原子操作時(shí),調(diào)用mtr_start生成一個(gè)mtr,mtr中會(huì)維護(hù)一個(gè)動(dòng)態(tài)增長(zhǎng)的m_log,這是一個(gè)動(dòng)態(tài)分配的內(nèi)存空間,將這個(gè)原子操作需要寫(xiě)的所有REDO先寫(xiě)到這個(gè)m_log中,當(dāng)原子操作結(jié)束后,調(diào)用mtr_commit將m_log中的數(shù)據(jù)拷貝到InnoDB的Log Buffer。
2、寫(xiě)入InnoDB Log Buffer
高并發(fā)的環(huán)境中,會(huì)同時(shí)有非常多的min-transaction(mtr)需要拷貝數(shù)據(jù)到Log Buffer,如果通過(guò)鎖互斥,那么毫無(wú)疑問(wèn)這里將成為明顯的性能瓶頸。為此,從MySQL 8.0開(kāi)始,設(shè)計(jì)了一套無(wú)鎖的寫(xiě)log機(jī)制,其核心思路是允許不同的mtr,同時(shí)并發(fā)地寫(xiě)Log Buffer的不同位置。不同的mtr會(huì)首先調(diào)用log_buffer_reserve函數(shù),這個(gè)函數(shù)里會(huì)用自己的REDO長(zhǎng)度,原子地對(duì)全局偏移log.sn做fetch_add,得到自己在Log Buffer中獨(dú)享的空間。之后不同mtr并行的將自己的m_log中的數(shù)據(jù)拷貝到各自獨(dú)享的空間內(nèi)。
- /* Reserve space in sequence of data bytes: */
- const sn_t start_sn = log.sn.fetch_add(len);
3、寫(xiě)入Page Cache
寫(xiě)入到Log Buffer中的REDO數(shù)據(jù)需要進(jìn)一步寫(xiě)入操作系統(tǒng)的Page Cache,InnoDB中有單獨(dú)的log_writer來(lái)做這件事情。這里有個(gè)問(wèn)題,由于Log Buffer中的數(shù)據(jù)是不同mtr并發(fā)寫(xiě)入的,這個(gè)過(guò)程中Log Buffer中是有空洞的,因此log_writer需要感知當(dāng)前Log Buffer中連續(xù)日志的末尾,將連續(xù)日志通過(guò)pwrite系統(tǒng)調(diào)用寫(xiě)入操作系統(tǒng)Page Cache。整個(gè)過(guò)程中應(yīng)盡可能不影響后續(xù)mtr進(jìn)行數(shù)據(jù)拷貝,InnoDB在這里引入一個(gè)叫做link_buf的數(shù)據(jù)結(jié)構(gòu),如下圖所示:
link_buf是一個(gè)循環(huán)使用的數(shù)組,對(duì)每個(gè)lsn取??梢缘玫狡湓趌ink_buf上的一個(gè)槽位,在這個(gè)槽位中記錄REDO長(zhǎng)度。另外一個(gè)線程從開(kāi)始遍歷這個(gè)link_buf,通過(guò)槽位中的長(zhǎng)度可以找到這條REDO的結(jié)尾位置,一直遍歷到下一位置為0的位置,可以認(rèn)為之后的REDO有空洞,而之前已經(jīng)連續(xù),這個(gè)位置叫做link_buf的tail。下面看看log_writer和眾多mtr是如何利用這個(gè)link_buf數(shù)據(jù)結(jié)構(gòu)的。這里的這個(gè)link_buf為log.recent_written,如下圖所示:
圖中上半部分是REDO日志示意圖,write_lsn是當(dāng)前l(fā)og_writer已經(jīng)寫(xiě)入到Page Cache中日志末尾,current_lsn是當(dāng)前已經(jīng)分配給mtr的的最大lsn位置,而buf_ready_for_write_lsn是當(dāng)前l(fā)og_writer找到的Log Buffer中已經(jīng)連續(xù)的日志結(jié)尾,從write_lsn到buf_ready_for_write_lsn是下一次log_writer可以連續(xù)調(diào)用pwrite寫(xiě)入Page Cache的范圍,而從buf_ready_for_write_lsn到current_lsn是當(dāng)前mtr正在并發(fā)寫(xiě)Log Buffer的范圍。下面的連續(xù)方格便是log.recent_written的數(shù)據(jù)結(jié)構(gòu),可以看出由于中間的兩個(gè)全零的空洞導(dǎo)致buf_ready_for_write_lsn無(wú)法繼續(xù)推進(jìn),接下來(lái),假如reserve到中間第一個(gè)空洞的mtr也完成了寫(xiě)Log Buffer,并更新了log.recent_written*,如下圖:
這時(shí),log_writer從當(dāng)前的buf_ready_for_write_lsn向后遍歷log.recent_written,發(fā)現(xiàn)這段已經(jīng)連續(xù):
因此提升當(dāng)前的buf_ready_for_write_lsn,并將log.recent_written的tail位置向前滑動(dòng),之后的位置清零,供之后循環(huán)復(fù)用:
緊接log_writer將連續(xù)的內(nèi)容刷盤(pán)并提升write_lsn。
4、刷盤(pán)
log_writer提升write_lsn之后會(huì)通知log_flusher線程,log_flusher線程會(huì)調(diào)用fsync將REDO刷盤(pán),至此完成了REDO完整的寫(xiě)入過(guò)程。
5、喚醒用戶線程
為了保證數(shù)據(jù)正確,只有REDO寫(xiě)完后事務(wù)才可以commit,因此在REDO寫(xiě)入的過(guò)程中,大量的用戶線程會(huì)block等待,直到自己的最后一條日志結(jié)束寫(xiě)入。默認(rèn)情況下innodb_flush_log_at_trx_commit = 1,需要等REDO完成刷盤(pán),這也是最安全的方式。當(dāng)然,也可以通過(guò)設(shè)置innodb_flush_log_at_trx_commit = 2,這樣,只要REDO寫(xiě)入Page Cache就認(rèn)為完成了寫(xiě)入,極端情況下,掉電可能導(dǎo)致數(shù)據(jù)丟失。
大量的用戶線程調(diào)用log_write_up_to等待在自己的lsn位置,為了避免大量無(wú)效的喚醒,InnoDB將阻塞的條件變量拆分為多個(gè),log_write_up_to根據(jù)自己需要等待的lsn所在的block取模對(duì)應(yīng)到不同的條件變量上去。同時(shí),為了避免大量的喚醒工作影響log_writer或log_flusher線程,InnoDB中引入了兩個(gè)專門(mén)負(fù)責(zé)喚醒用戶的線程:log_wirte_notifier和log_flush_notifier,當(dāng)超過(guò)一個(gè)條件變量需要被喚醒時(shí),log_writer和log_flusher會(huì)通知這兩個(gè)線程完成喚醒工作。下圖是整個(gè)過(guò)程的示意圖:
多個(gè)線程通過(guò)一些內(nèi)部數(shù)據(jù)結(jié)構(gòu)的輔助,完成了高效的從REDO產(chǎn)生,到REDO寫(xiě)盤(pán),再到喚醒用戶線程的流程,下面是整個(gè)這個(gè)過(guò)程的時(shí)序圖:
六、如何安全地清除REDO
由于REDO文件空間有限,同時(shí)為了盡量減少恢復(fù)時(shí)需要重放的REDO,InnoDB引入log_checkpointer線程周期性的打Checkpoint。重啟恢復(fù)的時(shí)候,只需要從最新的Checkpoint開(kāi)始回放后邊的REDO,因此Checkpoint之前的REDO就可以刪除或被復(fù)用。
我們知道REDO的作用是避免只寫(xiě)了內(nèi)存的數(shù)據(jù)由于故障丟失,那么打Checkpiont的位置就必須保證之前所有REDO所產(chǎn)生的內(nèi)存臟頁(yè)都已經(jīng)刷盤(pán)。最直接的,可以從Buffer Pool中獲得當(dāng)前所有臟頁(yè)對(duì)應(yīng)的最小REDO LSN:lwm_lsn。但光有這個(gè)還不夠,因?yàn)橛幸徊糠謒in-transaction的REDO對(duì)應(yīng)的Page還沒(méi)有來(lái)的及加入到Buffer Pool的臟頁(yè)中去,如果checkpoint打到這些REDO的后邊,一旦這時(shí)發(fā)生故障恢復(fù),這部分?jǐn)?shù)據(jù)將丟失,因此還需要知道當(dāng)前已經(jīng)加入到Buffer Pool的REDO lsn位置:dpa_lsn。取二者的較小值作為最終checkpoint的位置,其核心邏輯如下:
- /* LWM lsn for unflushed dirty pages in Buffer Pool */
- lsn_t lwm_lsn = buf_pool_get_oldest_modification_lwm();
- /* Note lsn up to which all dirty pages have already been added into Buffer Pool */
- const lsn_t dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log);
- lsn_t checkpoint_lsn = std::min(lwm_lsn, dpa_lsn);
MySQL 8.0中為了能夠讓mtr之間更大程度的并發(fā),允許并發(fā)地給Buffer Pool注冊(cè)臟頁(yè)。類似與log.recent_written和log_writer,這里引入一個(gè)叫做recent_closed的link_buf來(lái)處理并發(fā)帶來(lái)的空洞,由單獨(dú)的線程log_closer來(lái)提升recent_closed的tail,也就是當(dāng)前連續(xù)加入Buffer Pool臟頁(yè)的最大LSN,這個(gè)值也就是上面提到的dpa_lsn。需要注意的是,由于這種亂序的存在,lwm_lsn的值并不能簡(jiǎn)單的獲取當(dāng)前Buffer Pool中的最老的臟頁(yè)的LSN,保守起見(jiàn),還需要減掉一個(gè)recent_closed的容量大小,也就是最大的亂序范圍,簡(jiǎn)化后的代碼如下:
- /* LWM lsn for unflushed dirty pages in Buffer Pool */
- const lsn_t lsn = buf_pool_get_oldest_modification_approx();
- const lsn_t lag = log.recent_closed.capacity();
- lsn_t lwm_lsn = lsn - lag;
- /* Note lsn up to which all dirty pages have already been added into Buffer Pool */
- const lsn_t dpa_lsn = log_buffer_dirty_pages_added_up_to_lsn(log);
- lsn_t checkpoint_lsn = std::min(lwm_lsn, dpa_lsn);
這里有一個(gè)問(wèn)題,由于lwm_lsn已經(jīng)減去了recent_closed的capacity,因此理論上這個(gè)值一定是小于dpa_lsn的。那么再去比較lwm_lsn和dpa_lsn來(lái)獲取Checkpoint位置或許是沒(méi)有意義的。
上面已經(jīng)提到,ib_logfile0文件的前三個(gè)Block有兩個(gè)被預(yù)留作為Checkpoint Block,這兩個(gè)Block會(huì)在打Checkpiont的時(shí)候交替使用,這樣來(lái)避免寫(xiě)Checkpoint過(guò)程中的崩潰導(dǎo)致沒(méi)有可用的Checkpoint。Checkpoint Block中的內(nèi)容如下:
首先8個(gè)字節(jié)的Checkpoint Number,通過(guò)比較這個(gè)值可以判斷哪個(gè)是最新的Checkpiont記錄,之后8字節(jié)的Checkpoint LSN為打Checkpoint的REDO位置,恢復(fù)時(shí)會(huì)從這個(gè)位置開(kāi)始重放后邊的REDO。之后8個(gè)字節(jié)的Checkpoint Offset,將Checkpoint LSN與文件空間的偏移對(duì)應(yīng)起來(lái)。最后8字節(jié)是前面提到的Log Buffer的長(zhǎng)度,這個(gè)值目前在恢復(fù)過(guò)程并沒(méi)有使用。
七、總結(jié)
本文系統(tǒng)的介紹了InnoDB中REDO的作用、特性、組織結(jié)構(gòu)、寫(xiě)入方式已經(jīng)清理時(shí)機(jī),基本覆蓋了REDO的大多數(shù)內(nèi)容。關(guān)于重啟恢復(fù)時(shí)如何使用REDO將數(shù)據(jù)庫(kù)恢復(fù)到正確的狀態(tài),將在之后介紹InnoDB故障恢復(fù)機(jī)制的時(shí)候詳細(xì)介紹。
參考
[1] MySQL 8.0.11Source Code Documentation: Format of redo log
https://dev.mysql.com/doc/dev/mysql-server/8.0.11/PAGE_INNODB_REDO_LOG_FORMAT.html?spm=ata.21736010.0.0.600e6f95JcmTlA
[2] MySQL 8.0: New Lock free, scalable WAL design
https://mysqlserverteam.com/mysql-8-0-new-lock-free-scalable-wal-design/?spm=ata.21736010.0.0.600e6f95JcmTlA
[3] How InnoDB handles REDO logging
https://www.percona.com/blog/2011/02/03/how-innodb-handles-redo-logging/?spm=ata.21736010.0.0.600e6f95JcmTlA
[4] MySQL Source Code
https://github.com/mysql/mysql-server?spm=ata.21736010.0.0.600e6f95JcmTlA
[5] 數(shù)據(jù)庫(kù)故障恢復(fù)機(jī)制的前世今生
http://catkang.github.io/2019/01/16/crash-recovery.html?spm=ata.21736010.0.0.600e6f95JcmTlA