學(xué)習(xí)日志-mysql的日志系統(tǒng)

經(jīng)常聽DBA同事說,mysql可以恢復(fù)到半個(gè)月內(nèi)任意一秒的狀態(tài),是否好奇是怎么做到的?

我們還是從一個(gè)表的一條更新語句說起,更新一個(gè)ID為主鍵和有一個(gè)整形字段c的表,ID = 1的行,例如下面這條更新語句:

update T set c = c+1 where ID = 1;

先附上mysql的邏輯架構(gòu)圖:


mysql邏輯架構(gòu)圖.png

更新語句會按照基本架構(gòu)流程走一遍。

mysql的更新流程,涉及兩個(gè)重要的日志模塊,也就是我們今天要討論的日志系統(tǒng)的主要構(gòu)成:redo log(重做日志)和bin log(歸檔日志)。雖然非DBA的同學(xué)基本不會關(guān)注這兩個(gè)日志作用,但是redo log和bin log在設(shè)計(jì)上有很多可以借鑒的地方,這些設(shè)計(jì)思路可以用的平時(shí)的應(yīng)用開發(fā)中。

重要的日志模塊:redo log

mysql的數(shù)據(jù)更新,最終都要將數(shù)據(jù)寫進(jìn)磁盤。如果每一次更新,都要直接寫進(jìn)磁盤,就要在磁盤找到對應(yīng)的那條記錄,然后再更新,整個(gè)過程IO成本、查找成本都會很高。為了解決這個(gè)問題,mysql的設(shè)計(jì)者就采用了redo log來提升更新效率。

log和磁盤的配合寫入,就是mysql里常說的WAL技術(shù),WAL全程是Write-Ahead Logging,它的關(guān)鍵思路是先寫日志,再寫磁盤。
具體來說,當(dāng)有一條記錄需要更新的時(shí)候嗎,InnoDB會先將記錄寫到redo log里,并更新內(nèi)存,這個(gè)時(shí)候,更新就算完成了。同時(shí),InnoDB引擎會在適當(dāng)?shù)臅r(shí)候,將這個(gè)操作記錄更新到磁盤里面,而這個(gè)更新往往是操作系統(tǒng)比價(jià)空閑的時(shí)候。

如果某個(gè)時(shí)刻操作不是很多,InnoDB可以等空閑的時(shí)候再寫入磁盤,但如果操作很多,log中又寫滿了,該怎么辦呢?
InnoDB的redo log是固定大小的文件,比如可以配置為一組4個(gè)文件,每個(gè)文件大小是1GB,那么就總共可以記錄4GB的操作。redo log從頭開始寫,寫到末尾就又回到開頭循環(huán)寫,如下圖所示:


redo log示意圖.png

write pos是當(dāng)前的記錄的位置,一邊寫一邊后移,寫到3號文件末尾就又回到0號文件開頭。check point是當(dāng)前要擦除的位置,也是往后推移并且循環(huán)的,擦除記錄之前要把記錄更新到數(shù)據(jù)文件。

write pos和check point之間是空閑的部分,可以用來記錄新的操作。如果write pos追上了check point,標(biāo)識日志已經(jīng)寫滿 了,這個(gè)時(shí)候不能再執(zhí)行更新,得停下來先擦掉一些記錄,把check point往前推進(jìn)一下。

有了redo log,InnoDB就可以保證即時(shí)數(shù)據(jù)庫發(fā)生異常重啟,之前提交的記錄都不會丟失,這個(gè)能力稱之為crash-safe。

重要的日志模塊:binlog

從mysql的邏輯架構(gòu)圖來看,其實(shí)就兩塊:一塊是Server層,它主要做的是mysql功能層面的事情;另一塊是引擎層,負(fù)責(zé)存儲相關(guān)的具體事宜。
上面我們聊到的redo log是InnoDB引擎特有的日志,而Server層也有自己的日志--binlog(歸檔日志)。

為什么會有兩份日志系統(tǒng)呢?
因?yàn)樽铋_始mysql里沒有InnoDB引擎。mysql自帶的引擎是MyISAM,但是MyISAM沒有crash-safe能力,binlog日志只能用于歸檔。而InnoDB是另個(gè)一公司以插件形式引入mysql的,既然只依靠binlog是沒有crash-safe能力的,所以InnoDB使用另外一套日志系統(tǒng)--也就是redo log來實(shí)現(xiàn)crash safe能力。

這兩種日志系統(tǒng)有以下三點(diǎn)不同。

  1. redo log是InnoDB引擎特有的;binlog是mysql的server層實(shí)現(xiàn)的,所有引擎都可以使用
  2. redo log是物理日志,記錄的是在“在某個(gè)數(shù)據(jù)頁上做了什么修改”;binlog是邏輯日志,記錄的是這個(gè)語句的原始邏輯,比如“給ID = 1的這一行加1”.
  3. redo log是循環(huán)寫的,空間固定會用完;binlog是可以追加寫入的。“追加寫入”是指binlog文件寫到一定大小后會切換到下一個(gè),并不會覆蓋以前的日志。

有了對這兩個(gè)日志的概念性理解,我們再來看執(zhí)行器和InnoDB引擎在執(zhí)行這個(gè)簡單update語句時(shí)的內(nèi)部流程。

  1. 執(zhí)行器先找引擎取ID=1這一行。ID是主鍵,引擎直接使用樹搜索到這一樣。如果ID=1的這一行所在的數(shù)據(jù)頁本來就在內(nèi)存中,就直接返回給執(zhí)行器;否則,需要先從磁盤讀入內(nèi)存,然后再返回。
  2. 執(zhí)行器拿到引擎給的行數(shù)據(jù),把這個(gè)數(shù)據(jù)加上1,比如原來是N,現(xiàn)在就是N+1,得到新的一行數(shù)據(jù),再調(diào)用引擎接口寫入這行新數(shù)據(jù)。
  3. 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時(shí)將這個(gè)更新操作記錄到redo log里面,此時(shí)的redo log處于prepare狀態(tài)。然后告知執(zhí)行器完成了,可以隨時(shí)提交事務(wù)。
  4. 執(zhí)行器生成這個(gè)操作的binlog,并把binlog寫入磁盤。
  5. 執(zhí)行器調(diào)用引擎的提交事務(wù)接口,引擎把剛剛寫入的redo log改成提交(commit)狀態(tài),更新完成。

這里給出一個(gè)update語句的執(zhí)行流程圖,圖中淺色框表示在InnoDB內(nèi)部執(zhí)行的,深色框表示在執(zhí)行器中執(zhí)行的。


update語句執(zhí)行流程.png

你可能注意到了,最后三步有點(diǎn)“繞”,將redo log的寫入拆分成了兩個(gè)步驟:prepare和commit,這就是“兩階段提交”。

兩階段提交

為什么必須有“兩階段提交”呢?
這是為了讓兩份日志之間保持邏輯一致。要說明這個(gè)問題,我們得從開篇的問題說起:怎樣讓數(shù)據(jù)庫恢復(fù)到半個(gè)月內(nèi)任意一秒的狀態(tài)?

我們前面說過了,binlog日志會記錄所有的邏輯操作,并且采用“追加寫”的方式。如果你的DBA承諾說半個(gè)月內(nèi)可以恢復(fù),那么備份系統(tǒng)中一定會保存最近半個(gè)月的所有binlog,同時(shí)系統(tǒng)會定期做整庫備份。這里的“定期”取決于系統(tǒng)的重要性,可以是一天一備,也可以是一周一備。
當(dāng)需要回到指定的某一秒時(shí),比如某天下午兩點(diǎn)發(fā)現(xiàn)中午十二點(diǎn)有一次誤刪表,需要找回?cái)?shù)據(jù),那你可以這么做:

  1. 首先,找到最近的一次全量備份,如果你運(yùn)氣好,可能就是昨天晚上的一個(gè)備份,從這個(gè)備份恢復(fù)到臨時(shí)庫;
  2. 然后,從備份的時(shí)間點(diǎn)開始,將備份的binlog一次取出來,重放到中午誤刪表之前的那個(gè)時(shí)刻。

這樣你的臨時(shí)庫就跟誤刪之前的線上庫一樣了,然后你就可以把表數(shù)據(jù)從臨時(shí)庫取出來,按需要恢復(fù)到線上庫去。

說完了數(shù)據(jù)恢復(fù)過程,我們回來說為什么日志需要“兩階段提交”。這里不妨用反證法解釋一下。

由于redo log和binlog是兩個(gè)獨(dú)立的邏輯,如果不用兩階段提交,要么先寫完redo log再寫binlog,或者采用過來的順序。我們看看這樣會有什么問題。

仍然用前面的update來做栗子。假設(shè)當(dāng)前ID=1的行,字段c的值是0,再假設(shè)執(zhí)行update語句過程中在寫完第一個(gè)日志之后,第二個(gè)日志還沒有寫完期間發(fā)生了crash,會出現(xiàn)什么情況呢?

  1. 先寫redo log后寫binlog。假設(shè)在redo log寫完,binlog沒有寫完的時(shí)候,發(fā)生了異常重啟。按照之前的邏輯,從redo log恢復(fù)回來的數(shù)據(jù),恢復(fù)后這一行的數(shù)據(jù)c的值是1。
    但是由于binlog沒有記錄這個(gè)語句,因此之后備份日志的時(shí)候,存起來的binlog就沒有這條語句。
    然后如果使用這個(gè)binlog來恢復(fù)臨時(shí)庫,由于binlog的丟失,這個(gè)臨時(shí)庫就少了一次更新,導(dǎo)致了數(shù)據(jù)錯(cuò)誤。
  2. 先寫binlog再寫redo log。如果在binlog寫完之后crash,由于redo log還沒有寫,崩潰以后這個(gè)事務(wù)無效,所以這一行c的值是0,單binlog里面已經(jīng)記錄了“把c從0改成1”這個(gè)日志。所以之后用binlog來恢復(fù)的時(shí)候就多了一個(gè)事務(wù)出來,恢復(fù)出來的這樣c的值就是1,與原庫不同。

你可能會覺得,這個(gè)概率很低,平時(shí)也沒有動(dòng)不動(dòng)就要恢復(fù)臨時(shí)庫的場景呀。

其實(shí)不是的,不止誤操作的恢復(fù)數(shù)據(jù),當(dāng)你擴(kuò)容的時(shí)候,就是需要再多搭建一些備庫來增加系統(tǒng)的讀能力的時(shí)候,現(xiàn)在常見的做法也是用全量備份加上應(yīng)用binlog來實(shí)現(xiàn)的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容