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