經(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)圖:

更新語句會按照基本架構(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)寫,如下圖所示:

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)不同。
- redo log是InnoDB引擎特有的;binlog是mysql的server層實(shí)現(xiàn)的,所有引擎都可以使用
- redo log是物理日志,記錄的是在“在某個(gè)數(shù)據(jù)頁上做了什么修改”;binlog是邏輯日志,記錄的是這個(gè)語句的原始邏輯,比如“給ID = 1的這一行加1”.
- redo log是循環(huán)寫的,空間固定會用完;binlog是可以追加寫入的。“追加寫入”是指binlog文件寫到一定大小后會切換到下一個(gè),并不會覆蓋以前的日志。
有了對這兩個(gè)日志的概念性理解,我們再來看執(zhí)行器和InnoDB引擎在執(zhí)行這個(gè)簡單update語句時(shí)的內(nèi)部流程。
- 執(zhí)行器先找引擎取ID=1這一行。ID是主鍵,引擎直接使用樹搜索到這一樣。如果ID=1的這一行所在的數(shù)據(jù)頁本來就在內(nèi)存中,就直接返回給執(zhí)行器;否則,需要先從磁盤讀入內(nèi)存,然后再返回。
- 執(zhí)行器拿到引擎給的行數(shù)據(jù),把這個(gè)數(shù)據(jù)加上1,比如原來是N,現(xiàn)在就是N+1,得到新的一行數(shù)據(jù),再調(diào)用引擎接口寫入這行新數(shù)據(jù)。
- 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時(shí)將這個(gè)更新操作記錄到redo log里面,此時(shí)的redo log處于prepare狀態(tài)。然后告知執(zhí)行器完成了,可以隨時(shí)提交事務(wù)。
- 執(zhí)行器生成這個(gè)操作的binlog,并把binlog寫入磁盤。
- 執(zhí)行器調(diào)用引擎的提交事務(wù)接口,引擎把剛剛寫入的redo log改成提交(commit)狀態(tài),更新完成。
這里給出一個(gè)update語句的執(zhí)行流程圖,圖中淺色框表示在InnoDB內(nèi)部執(zhí)行的,深色框表示在執(zhí)行器中執(zhí)行的。

你可能注意到了,最后三步有點(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ù),那你可以這么做:
- 首先,找到最近的一次全量備份,如果你運(yùn)氣好,可能就是昨天晚上的一個(gè)備份,從這個(gè)備份恢復(fù)到臨時(shí)庫;
- 然后,從備份的時(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)什么情況呢?
-
先寫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ò)誤。 - 先寫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)的。