InnoDB如果發生意外當機了,資料會丢麼?
對于這個問題,稍微了解一點MySQL知識的人,都會斬釘截鐵的回答:不會!
為什麼?
他們也會毫不猶豫地說:因為有重做日志(redo log),資料可以通過redo log進行恢複。
回答得很好,那麼InnoDB怎樣通過redo log進行資料恢複的,具體的流程是怎樣的?
估計能說清楚這個問題的人所剩不多了,更深入一點:除了redo log,InnoDB在恢複過程中,還需要其他資訊麼?比如是否需要binlog參與?undo日志在恢複過程中又會起到什麼作用?
到這裡,可能很多人會變得疑惑起來:資料恢複跟undo有半毛錢的關系?
其實,InnoDB的資料恢複是一個很複雜的過程,這個恢複過程需要redo log、binlog、undo log等參與。這裡把InnoDB的恢複過程主要劃分為兩個階段,第一階段主要依賴于redo log的恢複;而第二階段,恰恰需要binlog和undo log的共同參與。
接下來,我們來具體了解下整個恢複的過程:
一、依賴redo log進行恢複第一階段,資料庫啟動後,InnoDB會通過redo log找到最近一次checkpoint的位置,然後根據checkpoint相對應的LSN開始,擷取需要重做的日志,接着解析擷取的日志并且儲存到一個哈希表中,最後通過周遊哈希表中的redo log資訊,讀取相關頁進行恢複。
InnoDB的checkpoint資訊儲存在日志檔案中,即ib_logfile0的開始2048個位元組中,checkpoint有兩個,交替更新,checkpoint與日志檔案的關系如下圖:

(checkpoint位置)
checkpoint資訊分别儲存在ib_logfile0的512位元組和1536位元組處,每個checkpoint預設大小為512位元組,InnoDB的checkpoint主要由3部分資訊組成:
● checkpoint no:主要儲存的是checkpoint号,因為InnoDB有兩個checkpoint,通過checkpoint号來判斷哪個checkpoint更新。
● checkpoint lsn:主要記錄了産生該checkpoint是flush的LSN,確定在該LSN前面的資料頁都已經落盤,不再需要通過redo log進行恢複。
● checkpoint offset:主要記錄了該checkpoint産生時,redo log在ib_logfile中的偏移量,通過該offset位置就可以找到需要恢複的redo log開始位置。
通過以上checkpoint的資訊,我們可以簡單得到需要恢複的redo log的位置,然後通過順序掃描該redo log來讀取資料,比如我們通過checkpoint定位到開始恢複的redo log位置在ib_logfile1中的某個位置,那麼整個redo log掃描的過程可能是這樣的:
(redo log掃描過程)
Step 1:從ib_logfile1的指定位置開始讀取redo log,每次讀取4 * page_size的大小,這裡我們預設頁面大小為16K,是以每次讀取64K的redo log到緩存中,redo log每條記錄(block)的大小為512位元組。
Step 2:讀取到緩存中的redo log通過解析、驗證等一系列過程後,把redo log的内容部分儲存到用于恢複的緩存recv_sys->buf,儲存到恢複緩存中的每條資訊主要包含兩部分:(space,offset)組成的位置資訊和具體redo log的内容,我們稱之為body。
Step 3:同時儲存在恢複緩存中的redo資訊會根據(space,offset)計算一個哈希值後儲存到一個哈希表(recv_sys->addr_hash)中,相同哈希值、不同(space,offset)用連結清單存儲,相同的(space,offset)用清單儲存,可能部分事務比較大,redo資訊一個block不能儲存,是以,每個body中可以用連結清單連結多body的值。
redo log被儲存到哈希表中之後,InnoDB就可以開始進行資料恢複,隻需要輪詢哈希表中的每個節點擷取redo資訊,根據(space,offset)讀取指定頁面後進行日志覆寫。
在上面整個過程中,InnoDB為了保證恢複的速度,做了幾點優化:
優化1:
在根據(space,offset)讀取資料頁資訊到buffer pool的時候,InnoDB不是隻讀取一張頁面,而是讀取相鄰的32張頁面到buffer pool。這裡有個假設,InnoDB認為,如果一張頁面被修改了,那麼其周圍的一些頁面很有可能也被修改了,是以一次性連續讀入32張頁面可以避免後續再重新讀取。
優化2:
在MySQL5.7版本以前,InnoDB恢複時需要依賴資料字典,因為InnoDB根本不知道某個具體的space對應的ibd檔案是哪個,這些資訊都是資料字典維護的。而且在恢複前,需要把所有的表空間全部打開,如果庫中有數以萬計的表,把所有表打開一遍,整個過程就會很慢。那麼MySQL5.7在這上面做了哪些改進呢?其實很簡單,針對上面的問題,InnoDB在redo log中增加了兩種redo log的類型來解決。MLOG_FILE_NAME用于記錄在checkpoint之後,所有被修改過的資訊(space,filepath);MLOG_CHECKPOINT則用于标志MLOG_FILE_NAME的結束。
上面兩種redo log類型的添加,完美解決了前面遺留的問題,redo log中儲存了後續需要恢複的space和filepath對。是以,在恢複的時候,隻需要從checkpoint的位置一直往後掃描到MLOG_CHECKPOINT的位置,這樣就能擷取到需要恢複的space和filepath。在恢複過程中,隻需要打開這些ibd檔案即可。當然由于space和filepath的對應關系通過redo存了下來,恢複的時候也不再依賴資料字典。
這裡需要強調的是MLOG_CHECKPOINT在每個checkpoint點中最多存在一次,如果出現多次MLOG_CHECKPOINT類型的日志,則說明redo已經損壞,InnoDB會報錯。
最多存在一次,那麼會不會有不存在的情況?
答案是肯定的,在每次checkpoint過後,如果沒有發生資料更新,那麼MLOG_CHECKPOINT就不會被記錄。是以隻要查找下redo log最新一個checkpoint後的MLOG_CHECKPOINT是否存在,就能判定上次MySQL是否正常關機。
5.7版本的MySQL在InnoDB進行恢複的時候,也正是這樣做的,MySQL5.7在進行恢複的時候,一般情況下需要進行最多3次的redo log掃描:
● 首先對redo log的掃描,主要是為了查找MLOG_CHECKPOINT,這裡并不進行redo log的解析。如果你沒有找到MLOG_CHECKPOINT,則說明InnoDB不需要進行recovery,後面的兩次掃描可以省略;如果找到了MLOG_CHECKPOINT,則擷取MLOG_FILE_NAME到指定清單,後續隻需打開該連結清單中的表空間即可。
● 下一步的掃描是在第一次找到MLOG_CHECKPOINT基礎之上進行的,該次掃描會把redo log解析到哈希表中,如果掃描完整個檔案,哈希表還沒有被填滿,則不需要第三次掃描,直接進行recovery就結束。
● 最後是在第二次基礎上進行的,第二次掃描把哈希表填滿後,還有redo log剩餘,則需要循環進行掃描,哈希表滿後立即進行recovery,直到所有的redo log被apply完為止。
redo log全部被解析并且apply完成,整個InnoDB recovery的第一階段也就結束了,在該階段中,所有已經被記錄到redo log但是沒有完成資料刷盤的記錄都被重新落盤。
然而,InnoDB單靠redo log的恢複是不夠的,這樣還是有可能會丢失資料(或者說造成主從資料不一緻)。
因為在事務送出過程中,寫binlog和寫redo log送出是兩個過程,寫binlog在前而redo送出在後,如果MySQL寫完binlog後,在redo送出之前發生了當機,這樣就會出現問題:binlog中已經包含了該條記錄,而redo沒有持久化。binlog已經落盤就意味着slave上可以apply該條資料,redo沒有持久化則代表了master上該條資料并沒有落盤,也不能通過redo進行恢複。
這樣就造成了主從資料的不一緻,換句話說主上丢失了部分資料,那麼MySQL又是如何保證在這樣的情況下,資料還是一緻的?這就需要進行第二階段恢複。
二、binlog和undo log共同參與前面提到,在第二階段恢複中,需要用到binlog和undo log,下面我們就來看下具體的恢複邏輯是怎樣的?
其實該階段的恢複中,也被劃分成兩部分:第一部分,根據binlog擷取所有可能沒有送出事務的xid清單;第二部分,根據undo中的資訊構造所有未送出事務連結清單,最後通過上面兩部分協調判斷事務是否可以送出。
(根據binlog擷取xid清單)
如上圖所示,MySQL在第二階段恢複的時候,先會去讀取最後一個binlog檔案的所有event資訊,然後把xid儲存到一個清單中,然後進行第二部分的恢複,如下:
(基于undo構造事務連結清單)
我們知道,InnoDB目前版本有128個復原段,每個復原段中儲存了undo log的位置指針,通過掃描undo日志,我們可以構造出還未被送出的事務連結清單(存在于insert_undo_list和update_undo_lsit中的事務都是未被送出的),是以通過起始頁(0,5)下的solt資訊可以定位到復原段,然後根據復原段下的undo的slot定位到undo頁,把所有的undo資訊建構一個undo_list,然後通過undo_list再建立未送出事務連結清單trx_sys->trx_list。
基于上面兩步, 我們已經建構了xid清單和未送出事務清單,那麼在這些未送出事務清單中的事務,哪些需要被送出?哪些又該復原?
判斷條件很簡單:凡是xid在通過binlog建構的xid清單中存在的事務,都需要被送出。換句話說,所有已經記錄binlog的事務,需要被送出,而剩下那些沒有記錄binlog的事務,則需要被復原。
三、回顧優化通過上述兩個階段的資料恢複,InnoDB才最終完成整個recovery過程,回過頭來我們再想想,在上述兩個階段中,是否還有優化空間?比如第一階段,在構造完哈希表後,事務的恢複是否可以并發進行?理論上每個hash node是根據(space,offset)生成的,不同的hash node之間不存在沖突,可以并行進行恢複。
或者在根據哈希表進行資料頁讀取時,每次讀取連續32張頁面,這裡讀取的32張頁面,可能有部分是不需要的,也同時被讀入到Buffer Pool中了,是否可以在建構一顆紅黑樹,根據(space,offset)組合鍵進行插入,這樣如果需要恢複的時候,可以根據紅黑樹的排序原理,把所有頁面的讀取順序化,并不需要讀取額外的頁面。
原文釋出時間為:2018-10-24
本文作者:蔣鴻翔
本文來自雲栖社群合作夥伴“
DBAplus社群”,了解相關資訊可以關注“
”。