全網(wǎng)最通透:MySQL 的 redo log 保證數(shù)據(jù)不丟的原理
總會(huì)有面試官問(wèn):你知道 MySQL 如何保障數(shù)據(jù)不丟的嗎?實(shí)際上這個(gè)問(wèn)題是十分不準(zhǔn)確的,MySQL 保障數(shù)據(jù)不丟的手段可太多了。但通常面試官想聽(tīng)的內(nèi)容就是 redo log 兩段式提交是如何保障數(shù)據(jù)不丟的。(不過(guò)個(gè)人感覺(jué)這么說(shuō)還是不太準(zhǔn)確)
所謂「redo log」,意即「重做日志」,也就是用來(lái)恢復(fù)數(shù)據(jù)用的日志。所謂「兩段式提交」,也被稱(chēng)作「兩階段提交」(Two-Phase Commit,簡(jiǎn)稱(chēng) 2PC)。本文主要講 MySQL 內(nèi)部 XA 事務(wù)中 redo log 兩段式提交的細(xì)節(jié)。為了讓大家飽餐一頓,我會(huì)先為大家上億點(diǎn)點(diǎn)前菜,雖然有點(diǎn)多,但是相信會(huì)很開(kāi)胃。
開(kāi)胃前菜
你知道什么是存儲(chǔ)引擎、隨機(jī) IO 和順序 IO嗎?你知道 MySQL 中的緩沖池嗎?binlog、redo log 聽(tīng)說(shuō)過(guò)嗎?都有什么用?什么?你都不知道?面試結(jié)束了。
什么是存儲(chǔ)引擎?
存儲(chǔ)引擎是 MySQL 中直接與磁盤(pán)交互部分。頁(yè)是存儲(chǔ)引擎讀寫(xiě)數(shù)據(jù)的最小單位,一個(gè)頁(yè)里可以有一條或多條表記錄。MySQL 中的存儲(chǔ)引擎有很多種,比如 InnoDB、MyISAM、Memory 等。其中最常用的是 InnoDB。而 InnoDB 是 MySQL 中唯一能夠完整支持事務(wù)特性的存儲(chǔ)引擎,也是一個(gè)高性能的存儲(chǔ)引擎。本文要講的「兩段式提交」就發(fā)生在 InnoDB 中。
什么是隨機(jī) IO 和順序 IO?
磁盤(pán)讀寫(xiě)數(shù)據(jù)的兩種方式。隨機(jī) IO 需要先找到地址,再讀寫(xiě)數(shù)據(jù),每次拿到的地址都是隨機(jī)的。就像送外賣(mài),每一單送的地址都不一樣,到處跑,效率極低。而順序 IO,由于地址是連貫的,找到地址后,一次可以讀寫(xiě)許多數(shù)據(jù),效率比較高。就像送外賣(mài),所有的單子地址都在一棟樓,一下可以送很多,效率很高。
什么是緩沖池?
關(guān)系型數(shù)據(jù)庫(kù)的特點(diǎn)就是需要對(duì)磁盤(pán)中大量的數(shù)據(jù)進(jìn)行存取,所以有時(shí)候也被叫做基于磁盤(pán)的數(shù)據(jù)庫(kù)。正是因?yàn)閿?shù)據(jù)庫(kù)需要頻繁對(duì)磁盤(pán)進(jìn)行 IO 操作,為了改善因?yàn)橹苯幼x寫(xiě)磁盤(pán)導(dǎo)致的 IO 性能問(wèn)題,所以引入了緩沖池。
緩沖池是一片內(nèi)存區(qū)域,存儲(chǔ)引擎在讀取數(shù)據(jù)時(shí),會(huì)先將頁(yè)讀取到緩沖池中。下次讀取時(shí),先判斷是否在緩沖池,如果在,則直接讀取,否則從磁盤(pán)中讀取。在修改數(shù)據(jù)時(shí),如果緩沖池中不存在所需的數(shù)據(jù)頁(yè),則從磁盤(pán)讀入緩沖池,否則直接對(duì)緩沖池中的數(shù)據(jù)頁(yè)進(jìn)行修改。
這樣的好處是,如果我們頻繁修改某一個(gè)位于磁盤(pán)的數(shù)據(jù)頁(yè),我們可以不用每次都去磁盤(pán)讀寫(xiě)(注意是讀和寫(xiě))該頁(yè),而是直接對(duì)緩沖池中的內(nèi)容修改,在一定的時(shí)機(jī)再把數(shù)據(jù)刷新到磁盤(pán)。這樣就會(huì)使得對(duì)磁盤(pán)的多次操作變?yōu)橐淮巍<幢阈薷牡膬?nèi)容在磁盤(pán)中相距較遠(yuǎn)的不同數(shù)據(jù)頁(yè)上,我們也可以將對(duì)多次對(duì)磁盤(pán)的 IO 合并為一次隨機(jī) IO。被修改的數(shù)據(jù)頁(yè)會(huì)與磁盤(pán)上的數(shù)據(jù)產(chǎn)生短暫的不一致,我們稱(chēng)此時(shí)緩沖池中的數(shù)據(jù)頁(yè)為 臟頁(yè) ,將該頁(yè)刷到磁盤(pán)的操作稱(chēng)為 刷臟頁(yè) (本句是重點(diǎn),后面要吃)。這個(gè)刷臟頁(yè)的時(shí)機(jī)我們看看就好:[^1]
innodb_max_dirty_pages_pct
由于這個(gè)刷臟頁(yè)的過(guò)程還是異步的,這樣更新操作就不需要等待磁盤(pán)的 IO 操作了。因此這些特點(diǎn)極大地提升了 InnoDB 的性能。
什么是binlog?
binlog 是 MySQL 服務(wù)器層面實(shí)現(xiàn)的一種二進(jìn)制日志,用于記錄所有對(duì)數(shù)據(jù)庫(kù)的更改操作(這種日志被稱(chēng)為邏輯日志)。比如你 update 一條記錄,服務(wù)器就會(huì)記錄一條對(duì)應(yīng)的信息到 binlog。但在 InnoDB 中,這個(gè) binlog 是以事務(wù)為單位刷新到磁盤(pán)的[^2]?;?binlog 的這種特性,一般我們會(huì)將 binlog 用于以下幾個(gè)方面:[^2]
數(shù)據(jù)庫(kù)增量備份與恢復(fù):在使用備份還原數(shù)據(jù)后,可以使用 binlog 中記錄的內(nèi)容對(duì)備份時(shí)間點(diǎn)(簡(jiǎn)稱(chēng)備份點(diǎn))后的數(shù)據(jù)進(jìn)行恢復(fù)。因?yàn)?binlog 會(huì)還會(huì)記錄下更改操作的時(shí)間,所以 binlog 可以恢復(fù)到某一具體時(shí)間點(diǎn)的數(shù)據(jù)。這就為我們刪庫(kù)后提供了除跑路以外的第二個(gè)選項(xiàng):使用 binlog 恢復(fù)數(shù)據(jù)。
主從復(fù)制:MySQL 從服務(wù)器可以通過(guò)訂閱 binlog 實(shí)現(xiàn)對(duì)主服務(wù)器的增量復(fù)制。
審計(jì):通過(guò)對(duì) binlog 中的數(shù)據(jù)進(jìn)行審計(jì),判斷是否存在安全問(wèn)題,比如 SQL 注入。
使用 binlog 進(jìn)行恢復(fù)的流程是:[^5]
- 先通過(guò)最新的備份恢復(fù)數(shù)據(jù)庫(kù)的數(shù)據(jù),并記錄下備份文件備份的時(shí)間點(diǎn)。
- 在 binlog 中找到這個(gè)時(shí)間點(diǎn),提取這個(gè)時(shí)間點(diǎn)以后的數(shù)據(jù)用于實(shí)現(xiàn)對(duì)備份點(diǎn)后數(shù)據(jù)的恢復(fù)(這個(gè)特性被稱(chēng)為 Point in Time,簡(jiǎn)稱(chēng) PIT)。
各個(gè)部分之間的關(guān)系
正餐開(kāi)始
食欲打開(kāi)了,后面的內(nèi)容我們就能吃的下了。
什么是 redo log?
前面我們講到數(shù)據(jù)頁(yè)在緩沖池中被修改會(huì)變成臟頁(yè)。如果這時(shí)宕機(jī),臟頁(yè)就會(huì)失效,這就導(dǎo)致我們修改的數(shù)據(jù)丟失了,也就無(wú)法保證事務(wù)的持久性。保證數(shù)據(jù)不丟,就是 redo log 的一個(gè)重要功能。我們已經(jīng)了解,如果我們修改了緩沖池中的數(shù)據(jù)頁(yè)就立刻刷臟頁(yè),會(huì)產(chǎn)生大量隨機(jī) IO,導(dǎo)致磁盤(pán)性能變差;但如果我們先寫(xiě)緩沖,一段時(shí)間后再刷臟頁(yè),就有可能造成數(shù)據(jù)丟失,無(wú)法保證事務(wù)的持久性。這可有點(diǎn)難了。
于是救世主來(lái)了,救世主的名字叫 WAL(Write-Ahead Logging,日志先行) 。即:事務(wù)提交前先寫(xiě)日志,再修改頁(yè)(修改頁(yè)的時(shí)機(jī)就是刷臟頁(yè)的時(shí)機(jī))。這里所謂的日志,就是 redo log。redo log 不會(huì)記錄對(duì)整個(gè)頁(yè)的修改,而是大概像這種:
xx 表空間,xx 頁(yè),xx 位置,xx 值
記錄下對(duì)磁盤(pán)中某某頁(yè)某某位置數(shù)據(jù)的修改結(jié)果(這種日志被稱(chēng)為物理日志),這樣會(huì)節(jié)省很多磁盤(pán)空間。 由于 redo log 是順序?qū)懀樞?IO),因此能有效提升 IO 效率;又因?yàn)槊看问聞?wù)提交前會(huì)先寫(xiě) redo log,因此可以保障更新的數(shù)據(jù)不丟失。
我們知道,一旦臟頁(yè)刷新,磁盤(pán)上對(duì)應(yīng)的 redo log 就會(huì)失效,所以 redo log 用完后,可以再回頭使用,這樣更節(jié)省空間。直到需要刷 redo log buffer 時(shí)發(fā)現(xiàn)接下來(lái)的 redo log 對(duì)應(yīng)的臟頁(yè)未被刷新,此時(shí)會(huì)強(qiáng)制刷新臟頁(yè)。緩沖池的好處我們前面已經(jīng)講過(guò),所以 redo log 弄了個(gè)類(lèi)似作用的 redo log buffer。在寫(xiě) redo log 時(shí)會(huì)先寫(xiě) redo log buffer,并在以下時(shí)機(jī)將 redo log 刷新到磁盤(pán):[^3]
- 每秒刷新一次
- 事務(wù)提交時(shí)
- redo log buffer 剩余空間小于 1/2 時(shí)
我們理應(yīng)想到,如果臟頁(yè)沒(méi)刷完,數(shù)據(jù)庫(kù)宕機(jī)了,那么必然是需要使用 redo log 來(lái)恢復(fù)數(shù)據(jù)的。那么 redo log 應(yīng)該從哪開(kāi)始恢復(fù)數(shù)據(jù)呢?為解決這個(gè)問(wèn)題 InnoDB 為 redo log 記錄了序列號(hào),這被稱(chēng)為 LSN(Log Sequence Number),可以理解為偏移量,越新的日志 LSN 越大。InnoDB 用檢查點(diǎn)( checkpoint_lsn? )指示未被刷盤(pán)的數(shù)據(jù)從這里開(kāi)始,用 lsn? 指示下一個(gè)應(yīng)該被寫(xiě)入日志的位置。不過(guò)由于有 redo log buffer 的緣故,實(shí)際被寫(xiě)入磁盤(pán)的位置往往比 lsn 要小。
為了大家能有個(gè)更整體的概念,咱們?cè)俣喑砸坏琅洳耍簎ndo log。InnoDB 能夠保證對(duì)事務(wù)的完整支持,這主要就得益于 redo log 和 undo log。redo log 我們講了,能夠保證緩沖池中被修改的數(shù)據(jù)頁(yè)不丟以及在數(shù)據(jù)庫(kù)宕機(jī)后對(duì)丟失的數(shù)據(jù)進(jìn)行自動(dòng)恢復(fù)。而 undo log 則用于實(shí)現(xiàn) MVCC 和事務(wù)回滾。在事務(wù)執(zhí)行的過(guò)程中,不但會(huì)記錄 redo log,還會(huì)記錄 undo log。至于更多細(xì)節(jié),大家自行去了解吧。
那么 redo log 到底如何保障數(shù)據(jù)不丟的?
如何保障數(shù)據(jù)不丟?
假設(shè)我們有一個(gè)表 t1,數(shù)據(jù)如下:
mysql> select * from t1;
+----+------+
| id | name |
+----+------+
| 1 | a |
+----+------+
當(dāng)我們執(zhí)行如下 update 語(yǔ)句時(shí):
mysql begin; update t1 set name='aa' where id=1; commit;
InnoDB 內(nèi)部的流程是這樣的:
- 服務(wù)器收到事務(wù)開(kāi)始的指令,為事務(wù)生成一個(gè)全局唯一的事務(wù) id。這個(gè)事務(wù) id 在記錄 binlog 和 redo log 時(shí)都會(huì)使用。
- 如果緩存池中沒(méi)有 id=1 所在數(shù)據(jù)頁(yè)的數(shù)據(jù),從磁盤(pán)中找到對(duì)應(yīng)的數(shù)據(jù)頁(yè)(注意,這里是一個(gè)數(shù)據(jù)頁(yè),不是一條記錄),把數(shù)據(jù)頁(yè)加載到緩存。
- 修改緩存數(shù)據(jù)頁(yè)中 id=1 的數(shù)據(jù)。
- 記錄數(shù)據(jù)到 redo log buffer[^4]、binlog cache[^2]。根據(jù) redo log 刷盤(pán)的策略,這個(gè)過(guò)程中 redo log buffer 可能會(huì)被刷新到磁盤(pán)。
- 服務(wù)器收到事務(wù)提交的指令。
- 刷新 redo log buffer 到磁盤(pán),并標(biāo)記該事務(wù)的狀態(tài)為 prepare。此操作稱(chēng)為 redo log prepare。
- 刷新 binlog cache 到磁盤(pán)。
- 刷新 redo log buffer 到磁盤(pán),并標(biāo)記該事務(wù)的狀態(tài)為 commit。此操作稱(chēng)為 redo log commit。
- 向客戶端返回事務(wù)執(zhí)行的結(jié)果。
這樣 redo log 先 prepare,再刷新 binlog ,再 redo log commit 的過(guò)程就是一次兩段式提交。這種只在 MySQL 內(nèi)部組件間保障數(shù)據(jù)一致性的操作,也被稱(chēng)作內(nèi)部 XA 事務(wù);與之對(duì)應(yīng)的是,保障跨服務(wù)器間數(shù)據(jù)一致性的兩段式提交,被稱(chēng)為外部 XA 事務(wù),即分布式事務(wù)。
注:XA 事務(wù)屬于分布式事務(wù)中兩段式提交事務(wù)的一種實(shí)現(xiàn)
在宕機(jī)后,重啟 MySQL 時(shí),InnoDB 會(huì)自動(dòng)恢復(fù) redo log 中 checkpoint_lsn 后的,且處于 commit 狀態(tài)的事務(wù)。如果 redo log 中事務(wù)的狀態(tài)為 prepare,則需要先查看 binlog 中該事務(wù)是否存在,是的話就恢復(fù),否則就回滾(通過(guò) undo log 回滾。臟頁(yè)一直在刷,更新了臟頁(yè),但事務(wù)沒(méi)提交就宕機(jī)了,所以需要回滾)。
消化一下
發(fā)生宕機(jī)怎么辦?
MySQL 宕機(jī)可能會(huì)發(fā)生在整個(gè)過(guò)程中的任意時(shí)刻。以剛才的流程為例,假設(shè)宕機(jī)發(fā)生在第 5 步后、第 6 步前。此時(shí)服務(wù)器還未向客戶端返回事務(wù)的結(jié)果,而 redo log 中可能記錄了該事務(wù)的 redo log,也可能沒(méi)有。但是只要該事務(wù)沒(méi)有被標(biāo)記為 prepare,我們就認(rèn)為該事務(wù)沒(méi)有執(zhí)行完,否則 redo log 用于恢復(fù)事務(wù)的數(shù)據(jù)可能是不完整的。因此,只要此時(shí)我們選擇拋棄未 prepare 的 redo log,不會(huì)導(dǎo)致任何數(shù)據(jù)一致性的問(wèn)題。
那么后面的步驟宕機(jī)會(huì)怎樣呢?這就涉及到為什么非得要兩階段提交了。
為什么非得要兩階段提交?
在說(shuō)明以前,我們還需要弄清兩個(gè)問(wèn)題:
- 有 binlog 為什么還要 redo log ?
- 有 redo log 為什么還要 binlog?
有 binlog 為什么還要 redo log ?
- binlog 不知道數(shù)據(jù)庫(kù)究竟是在哪一時(shí)刻丟失了哪部分?jǐn)?shù)據(jù),只能從備份點(diǎn)開(kāi)始對(duì) binlog 記錄重放來(lái)恢復(fù)數(shù)據(jù),比較耗時(shí)。
- binlog 恢復(fù)是需要我們手動(dòng)執(zhí)行的,而 redo log 可以在服務(wù)器重啟后自動(dòng)恢復(fù)數(shù)據(jù)。
- WAL + 先寫(xiě)緩沖 + 異步刷臟頁(yè)有效提升了磁盤(pán)的 IO 效率。
有 redo log 為什么還要 binlog?
- binlog 是服務(wù)器層面的功能,redo log 是 innoDB 的功能。redo log 幫助 InnoDB 實(shí)現(xiàn)了性能提升、自動(dòng)恢復(fù)。但其他存儲(chǔ)引擎是無(wú)法使用 redo log 的能力的。
- 我們也可以關(guān)閉 binlog,但大多數(shù)情況下我們都會(huì)開(kāi)啟,因?yàn)殚_(kāi)啟的好處更多。比如,主從模式需要訂閱 binlog 進(jìn)行主從復(fù)制,以及可以通過(guò) binlog 進(jìn)行數(shù)據(jù)庫(kù)的增量備份和恢復(fù)。
redo log 有很多好處,所以我們不能放棄;binlog 也有很多好處,我們也不能放棄。也就是說(shuō),這兩個(gè)功能我們都需要開(kāi)啟。既然都要開(kāi)啟,那么 我們必須保證 redo log 和 binlog 數(shù)據(jù)的一致性。 如果 binlog 有 redo log 沒(méi)有,那么 redo log 宕機(jī)自動(dòng)恢復(fù)時(shí)的數(shù)據(jù)就會(huì)缺少;反之,redo log 有,binlog 沒(méi)有,如果開(kāi)啟了主從模式,主服務(wù)器因?yàn)?redo log 恢復(fù)了數(shù)據(jù),但從服務(wù)器靠消費(fèi) binlog 保證和主服務(wù)器數(shù)據(jù)一致,這就導(dǎo)致從服務(wù)器比主服務(wù)器數(shù)據(jù)少。
那么為什么非得要寫(xiě)兩次,我們能不能只寫(xiě)一次 redo log?
這樣仍然會(huì)有不一致問(wèn)題。比方說(shuō),先寫(xiě) binlog 再寫(xiě) redo log:
此時(shí)如果有大量并發(fā),我們 binlog 噌噌噌往上寫(xiě),redo log 還沒(méi)寫(xiě)完,宕機(jī)機(jī)了,兩者的數(shù)據(jù)就會(huì)出現(xiàn)大量不一致現(xiàn)象。此外,因?yàn)?binlog 數(shù)據(jù)最完整,這樣會(huì)導(dǎo)致我們必須從 binlog 回滾,而且還得是手動(dòng)回滾。InnoDB 本來(lái)是可以自恢復(fù)的存儲(chǔ)引擎,這樣一來(lái),自恢復(fù)的特性不是沒(méi)了,redo log 不是白開(kāi)發(fā)了?使用 binlog 恢復(fù) redo log 更不用想了,因?yàn)?binlog 根本不知道從何處開(kāi)始恢復(fù)(它沒(méi)有 checkpoint_lsn )。
再說(shuō)先寫(xiě) redo log 再寫(xiě) binlog:
不一致性的問(wèn)題與上述內(nèi)容相似。另外還會(huì)導(dǎo)致 redo log 在恢復(fù)時(shí),每次都需要去 binlog 查看該事務(wù)是否已寫(xiě)入,嚴(yán)重影響性能。而如果是兩階段提交,處于 commit 階段的事務(wù)都會(huì)直接恢復(fù),處于 prepare 階段才需要去看 binlog。
那用 redo log 恢復(fù) binlog 不行嗎?
第一,binlog 是服務(wù)器的特性,redo log 是 InnoDB 的特性,兩者并不在一個(gè)層面上,能不能這么做,很難說(shuō)。第二,即便可以,也增加了很大的復(fù)雜度, redo log 中記錄的數(shù)據(jù)(物理日志)能不能復(fù)原 SQL 語(yǔ)句,如何復(fù)原,這都是需要思考的問(wèn)題。遠(yuǎn)遠(yuǎn)不如直接使用兩階段提交方便。
兩段式提交會(huì)不會(huì)影響性能?
InnoDB 使用了組提交的方式,盡量降低了兩階段提交帶來(lái)的性能影響。在并發(fā)事務(wù)較多的情況下,MySQL 會(huì)將多個(gè)事務(wù)的 redo log 放在一起提交,大大節(jié)省了磁盤(pán) IO。具體就不在此展開(kāi)了。binlog 刷盤(pán)時(shí)同樣也會(huì)采取類(lèi)似的策略。
吃點(diǎn)飯后甜點(diǎn)吧
如果你搞明白了上面的內(nèi)容,你會(huì)發(fā)現(xiàn)「基于事務(wù)消息的分布式事務(wù)」使用的就是典型的 2PC 思想,你又會(huì)發(fā)現(xiàn)「基于本地消息的分布式事務(wù)」使用的就是典型的 WAL 思想。如果你不了解,馬上去學(xué)一下吧!