糟了,數(shù)據(jù)庫崩了,又好像沒崩
前言
2023 年某一天周末,新手程序員小明因為領(lǐng)導安排的一個活來到公司加班,小明三下五除二,按照領(lǐng)導要求寫了一個跑批的數(shù)據(jù)落庫任務(wù)在測試環(huán)境執(zhí)行 ,突然間公司停電了,小明大驚,“糟了,MySQL 還在跑任務(wù),會不會因為突然斷電,導致數(shù)據(jù)庫崩了”。
這時候,傍邊的同事云淡風清的說了一句,“沒事,小明,MySQL 有一套預(yù)寫日志機制就是應(yīng)對這種情況的。你的落庫任務(wù)啟用了事務(wù)沒,啟用了的話,就等來電重新跑一下任務(wù)就行了?!?/p>
聽了同事的話,小明懸著的心放了下來?!芭杜?,我啟用了事務(wù),那我還是等周一來重新跑一遍”。
回家的公交車上,小明默默的打開百度,搜索 MySQL 預(yù)寫日志 ,寫下了這篇文章 ??。
本文思維導圖
什么是預(yù)寫日志機制?
一般情況下,大部分數(shù)據(jù)庫都是將表和索引存儲在磁盤文件中。當新增數(shù)據(jù)時,數(shù)據(jù)庫系統(tǒng)會先寫入內(nèi)存,然后將其寫入磁盤上的數(shù)據(jù)文件。
那為什么不直接寫入磁盤嘞?主要是每次新增都直接寫入磁盤性能很低,放在內(nèi)存中,可以批量寫入磁盤以提升性能。
但有一個問題,如果數(shù)據(jù)在寫入磁盤文件中途斷電怎么辦?當來電恢復(fù)后,我們重啟數(shù)據(jù)庫,發(fā)現(xiàn)數(shù)據(jù)不一致,又該如何處理。
所以我們需要一些其他機制來避免斷電引發(fā)的數(shù)據(jù)不一致,其實 MySQL 已經(jīng)考慮到了這一點,內(nèi)部已經(jīng)實現(xiàn)一套 WAL(預(yù)寫日志)機制來避免這一點。
MySQL 設(shè)計有健壯的恢復(fù)機制,特別是使用 InnoDB 存儲引擎的情況下,它能夠在斷電后重啟而不會崩潰。InnoDB 存儲引擎使用預(yù)寫日志(WAL)機制來確保數(shù)據(jù)的一致性和原子性。
預(yù)寫日志機制是一種數(shù)據(jù)庫事務(wù)日志技術(shù),它要求在任何數(shù)據(jù)庫修改被寫入到永久存儲(也就是磁盤)之前,先將這些修改記錄到日志中。
這樣當 MySQL 遇到意外的斷電情況時,它會在重啟后利用 Redo log 來恢復(fù)已提交但未寫入數(shù)據(jù)文件的事務(wù)繼續(xù)寫入數(shù)據(jù)文件,從而保證一致性,再利用 undo log 來撤銷未提交事務(wù)的需改,從而保證原子性。
MySQL 中的預(yù)寫日志機制
在 MySQL 中,InnoDB 存儲引擎實現(xiàn)了 WAL 機制。包含 Redo log buffer、Redo log、Undo Log 等,來記錄事務(wù)已提交但未寫入數(shù)據(jù)文件的數(shù)據(jù)變更以及事務(wù)回滾后的數(shù)據(jù)還原。
為了給大家講清楚 MySQL 的預(yù)寫日志機制,會涉及到 MySQL 架構(gòu)中的以下內(nèi)容,
Buffer Pool(緩沖池)
Buffer Pool (緩沖池)是 InnoDB 存儲引擎中非常重要的內(nèi)存結(jié)構(gòu),顧名思義,緩沖池就是起到一個緩存的作用,因為我們都知道 MySQL 的數(shù)據(jù)最終是存儲在磁盤中的,如果沒有這個 Buffer Pool 那么我們每次的數(shù)據(jù)庫請求都會磁盤中查找,這樣必然會存在 IO 操作,這肯定是無法接受的。
但是有了 Buffer Pool 就是我們第一次在查詢的時候會將查詢的結(jié)果存到 Buffer Pool 中,這樣后面再有請求的時候就會先從緩沖池中去查詢,如果沒有再去磁盤中查找,然后在放到 Buffer Pool 中。
Redo log buffer(日志緩沖區(qū))
Redo log buffer 是用作數(shù)據(jù)變更記錄寫入 Redo log 文件前的一塊內(nèi)存區(qū)域。日志緩沖區(qū)大小由 innodb_log_buffer_size 變量定義,默認大小為 16MB。
日志緩沖區(qū)的內(nèi)容會定期刷新到 Redo log 文件中,大型日志緩沖區(qū)允許大型事務(wù)運行,而無需在事務(wù)提交之前將 Redo log 數(shù)據(jù)寫入磁盤。因此如果事務(wù)涉及的更新、插入或刪除操作數(shù)據(jù)量較大時,可以增加日志緩沖區(qū)的大小可以節(jié)省磁盤 I/O。
MySQL 提交事務(wù)的時候,會將 Redo log buffer 中的數(shù)據(jù)寫入到 Redo log 文件中,刷磁盤可以通過 innodb_flush_log_at_trx_commit 參數(shù)來設(shè)置
- 值為 0 表示不刷入磁盤
 - 值為 1 表示立即刷入磁盤
 - 值為 2 表示先刷到 os cache
 
為了提高性能,MySQL 首先將修改操作寫入到日志緩沖區(qū),之后以 innodb_flush_log_at_trx_commit 參數(shù)設(shè)置落盤時機,將日志緩沖區(qū)刷入到磁盤的 Redo log 文件中去。
Redo Log
MySQL Redo Log 是 InnoDB 存儲引擎中的一個重要組件,它是一種磁盤基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),用于在崩潰重啟期間修復(fù)由已提交事務(wù)但未寫入數(shù)據(jù)文件的數(shù)據(jù)。
在正常操作中,Redo log 記錄了由 SQL 語句執(zhí)行導致的表數(shù)據(jù)變更記錄。將 Redo log buffer 中的數(shù)據(jù)持久化到磁盤中,就是將 Redo log buffer 中的數(shù)據(jù)寫入到 Redo log 磁盤文件中。
數(shù)據(jù)在由 Redo log buffer 寫入 Redo log 時的觸發(fā)時機如下,
- MySQL 正常關(guān)閉時觸發(fā)
 - 當 Redo log buffer 中記錄的寫入量大于 Redo log buffer 內(nèi)存空間的一半時,會觸發(fā)落盤
 - InnoDB 的后臺線程每隔 1 秒,將 Redo log buffer 持久化到磁盤
 - 每次事務(wù)提交時都將緩存在 redo log buffer 里的 redo log 直接持久化到磁盤(這個策略就是由上文提高 innodb_flush_log_at_trx_commit 參數(shù)控制)
 
Redo log 是 WAL 機制的核心,它記錄了事務(wù)所做的所有修改。如果數(shù)據(jù)庫發(fā)生故障,可以使用 Redo 日志來重做事務(wù),從而確保數(shù)據(jù)的一致性。
Undo Log
Undo Log 記錄了如何撤銷一個事務(wù)的修改。如果需要回滾事務(wù)或在執(zhí)行事務(wù)時還未提交,數(shù)據(jù)庫就發(fā)生了崩潰,這時我們就需要將未提交事務(wù)前的數(shù)據(jù)回滾回去,難道這個操作有我們自己來做嗎?顯然 MySQL 也考慮到了這一點。
MySQL 會使用 Undo log 來撤銷未提交的修改。在操作數(shù)據(jù)前,MySQL 首先將數(shù)據(jù)備份到 Undo log,然后進行數(shù)據(jù)修改。
如果出現(xiàn)錯誤或者用戶執(zhí)行了 Rollback 語句,系統(tǒng)可以利用 Undo log 中的備份將數(shù)據(jù)恢復(fù)到事務(wù)操作前的狀態(tài)。
通過 Undo log 撤銷修改,從而確保數(shù)據(jù)的原子性。
結(jié)合 Buffer Pool、Redo log buffer、Redo log、Undo log 后,我們在MySQL 中更新一條數(shù)據(jù)的流程如下,
圖片
圖片來源https://pdai.tech/md/db/sql-mysql/sql-mysql-execute.html
- 準備更新一條 SQL 語句
 - MySQL(innodb)會先去緩沖池(Buffer Pool)中去查找這條數(shù)據(jù),沒找到就會去磁盤中查找,如果查找到就會將這條數(shù)據(jù)加載到緩沖池(Buffer Pool)中
 - 在加載到 Buffer Pool 的同時,會將這條數(shù)據(jù)的原始記錄保存到 undo 日志文件中
 - innodb 會在 Buffer Pool 中執(zhí)行更新操作
 - 更新后的數(shù)據(jù)會記錄在 Redo log buffer 中
 - MySQL 提交事務(wù)的時候,會將 Redo log buffer 中的數(shù)據(jù)寫入到 Redo log 文件中,刷磁盤可以通過 innodb_flush_log_at_trx_commit 參數(shù)來設(shè)置
 - MySQL 重啟的時候會將 Redo log 恢復(fù)到緩沖池中
 
額外知識:檢查點(Checkpoint)
檢查點是什么?為什么有了 Redo log、Undo log 還要引入檢查點。
明明借助 Redo log、Undo log 我們就可以實現(xiàn) MySQL 的故障恢復(fù)了。
雖然數(shù)據(jù)在寫入 Redo log 文件后,就代表數(shù)據(jù)變更已經(jīng)生效了,但是還未寫入到數(shù)據(jù)文件,也就是還沒有完成事務(wù)的持久性。
那么檢查點就是幫助 MySQL 實現(xiàn)事務(wù)的持久性。
如果說 Redo log 可以無限地增大,能夠保存所有數(shù)據(jù)庫變更的數(shù)據(jù),那么在發(fā)生宕機時完全可以通過 Redo log 來恢復(fù)數(shù)據(jù)庫系統(tǒng)的數(shù)據(jù)到宕機發(fā)生前的情況。
然而現(xiàn)實是我們的物理磁盤文件大小是有效的。即使達成無限了,如果數(shù)據(jù)庫運行了很久后發(fā)生宕機,那么使用 Redo log 進行恢復(fù)的時間也會非常的久。
所以在 Redo log 文件容量是有限的情況下,還需要定期將 Redo log 寫入數(shù)據(jù)文件完成數(shù)據(jù)的持久化,在這樣的情況下,就引入了 Checkpoint(檢查點)技術(shù)。
Checkpoint(檢查點)技術(shù)不僅僅是會同步 Redo log 寫入數(shù)據(jù)文件,也會同步臟頁數(shù)據(jù)寫入數(shù)據(jù)文件。
檢查點的觸發(fā)時機有兩種如下,
Sharp Checkpoint(完全檢查點)
將內(nèi)存中所有臟頁全部寫到磁盤就是完全檢查點,比如數(shù)據(jù)庫實例關(guān)閉時。
Fuzzy Checkpoint(模糊檢查點)
將部分臟頁刷新到磁盤,就是模糊檢查點,一般就是臟頁達到一定數(shù)量時觸發(fā)。數(shù)據(jù)庫實例運行過程產(chǎn)生的檢查基本上就是這種類型的檢查點。
因此其實 Checkpoint 就是指一個觸發(fā)點(時間點),當發(fā)生 Checkpoint 時,會將臟頁寫回磁盤,以確保數(shù)據(jù)的持久性和一致性。并且 Redo log、Undo log 文件也可以重新覆寫,這樣可以保證重啟時不會因為 Redo log、Undo log 文件太大而導致重啟時間過長。
斷電故障恢復(fù)案例
OK,假如我們正在使用 MySQL 添加數(shù)據(jù)。在提交事務(wù)的過程中,突然發(fā)生了斷電,那么這個數(shù)據(jù)會丟嗎?
我們結(jié)合上文MySQL 中更新一條數(shù)據(jù)的流程,來給大家分析下具體場景,
數(shù)據(jù)在寫入 Buffer Pool、Redo log buffer 中時,發(fā)生斷電
先說結(jié)論,會丟。因為數(shù)據(jù)沒有寫入 Redo log 前,MySQL 是沒辦法保證數(shù)據(jù)一致性的。但是這沒關(guān)系的,因為 MySQL 會認為本次事務(wù)是失敗的,在重啟后可以根據(jù) Undo log 文件將數(shù)據(jù)恢復(fù)到更新前的樣子,并不會有任何的影響。
數(shù)據(jù)在寫入 Redo log 文件后,發(fā)生斷電
先說結(jié)論,不會丟。因為 Redo log buffer 中的數(shù)據(jù)已經(jīng)被寫入到 Redo log 了,就算數(shù)據(jù)庫宕機了,在下次重啟的時候 MySQL 也會將 Redo log 文件內(nèi)容恢復(fù)到 Buffer Pool 中進行重放。
參考資料
- https://xiaolincoding.com/mysql/log/how_update.html#redo-log-%E6%96%87%E4%BB%B6%E5%86%99%E6%BB%A1%E4%BA%86%E6%80%8E%E4%B9%88%E5%8A%9E
 - https://pdai.tech/md/db/sql-mysql/sql-mysql-execute.html
 - https://zhuanlan.zhihu.com/p/552706911?utm_medium=referral
 















 
 
 












 
 
 
 