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