我們執(zhí)行一個普通的update語句時,mysql底層會做些什么最終將數(shù)據(jù)持久化到磁盤呢?
疑問?
mysql中執(zhí)行更新操作時,必然涉及到讀、寫內(nèi)存、寫磁盤的操作流程。mysql是通過什么樣的操作去完成更新流程的優(yōu)化?
1. update語句執(zhí)行流程
update T set c = c+1 where id = 2;
- 執(zhí)行器先調(diào)用存儲引擎的接口獲取“id=2”的數(shù)據(jù)行。如果這一行所在的數(shù)據(jù)頁在內(nèi)存中,則存儲引擎直接返回給執(zhí)行器;否則需要存儲引擎先去磁盤中獲取數(shù)據(jù),讀取到內(nèi)存中,然后再返回。
- 執(zhí)行器拿到存儲引擎返回的這行數(shù)據(jù),對其進行更新操作,將c的值加+1,得到新的數(shù)據(jù),在調(diào)用存儲引擎接口,寫入這行數(shù)據(jù)。
- 存儲引擎收到執(zhí)行器寫入的這行數(shù)據(jù)的新結(jié)果,先將這條更新記錄保存在內(nèi)存(Buffer Pool)中,并將這條更新記錄寫入
redo log Buffer,更新redo log的狀態(tài)為prepare,隨后向執(zhí)行器返回結(jié)果。 - 執(zhí)行器知道存儲引擎已經(jīng)將這條更新記錄成功寫入redo log Buffer(內(nèi)存)后。
- 當SQL事務提交時,redo log 調(diào)用fsync寫入磁盤(默認策略是事務提交寫入磁盤)。
- 當SQL事務提交時,binlog調(diào)用fsync寫入磁盤(默認策略是事務提交寫入磁盤)。
- 在執(zhí)行器寫入binlog成功后,存儲引擎將redo log的狀態(tài)更新為commit,此時才算SQL事務正在提交成功;

2. redo log詳解
2.1 為什么要引入redo log
為了減少與磁盤的IO交互,在對數(shù)據(jù)庫增改刪操作時,實際主要都是針對內(nèi)存里的Buffer Pool中的數(shù)據(jù)進行的。
最詳細的MySQL事務特性及原理講解?。ㄒ唬?/a>
理解Mysql中的Buffer pool
InnoDB作為mysql的存儲引擎,數(shù)據(jù)是存放在磁盤中的,但是每次讀寫數(shù)據(jù)需要磁盤IO,效率就很低。為此,InnoDB提供了緩存(Buffer Pool),Buffer Pool中包含了磁盤中部分數(shù)據(jù)頁的映射,作為訪問數(shù)據(jù)庫的緩沖:

- 讀取數(shù)據(jù)時,首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則加載磁盤中的數(shù)據(jù)到Buffer Pool中;
- 寫入數(shù)據(jù)的時候,會首先寫入Buffer Pool,Buffer Pool中修改的數(shù)據(jù)會定期刷新到磁盤(這一過程被稱為刷臟)。
Buffer Pool的使用大大提高了讀寫數(shù)據(jù)的效率,但是也帶來了新的問題:如果mysql宕機,如何保證數(shù)據(jù)不丟失?
2.2 redo log如何保證數(shù)據(jù)不丟失?
在修改數(shù)據(jù)時,除了修改Buffer Pool中的數(shù)據(jù),還會在redo log Buffer(內(nèi)存)記錄這次操作。當事務提交時,會調(diào)用fsync對redo log(磁盤)進行刷盤。重啟時可以讀取磁盤上的redo log中的數(shù)據(jù),對數(shù)據(jù)庫進行恢復。redo log采用的是WAL技術(shù)(Write-ahead logging,預寫式日志)。
2.3 WAL技術(shù)?
2.3.1 簡介
目的:傳統(tǒng)磁盤的順序訪問性能遠好于隨機訪問,利用順序?qū)慙og來記錄對數(shù)據(jù)庫的操作,并在故障恢復后通過Log恢復數(shù)據(jù)庫到正確的狀態(tài);
算法:采用的是ARIES算法來實現(xiàn),Log同時記錄的是Redo和Undo的信息。原因就是BufferPool可以采用steal/no force的方式進行刷盤。
2.3.2 簡述
由于傳統(tǒng)磁盤順序訪問性能遠好于隨機訪問,采用Logging的故障恢復機制意圖利用順序?qū)懙腖og來記錄對數(shù)據(jù)庫的操作,并在故障恢復后通過Log內(nèi)容將數(shù)據(jù)庫恢復到正確的狀態(tài)。簡單來說,每次修改內(nèi)容前先順序?qū)憣膌og,同時為了保證恢復時可以從Log中看到最新的數(shù)據(jù)庫狀態(tài),要求Log先于數(shù)據(jù)內(nèi)容落盤,也就是常說的Write Ahead Log,WAL。
除此之外,事務完成Commit前還需要在Log中記錄對應的Commit標記,以便恢復是了解當時的事務狀態(tài),因此還需要關(guān)注Commit標記和事務中數(shù)據(jù)落盤順序。根據(jù)Log中記錄的內(nèi)容可以分為三類:Undo-Only、Redo-Only、Redo-Undo。
1、Undo Only Logging
Redo Log中記錄Log記錄可以表示為<T,X,v>:事務T修改了X的值,X的舊值是v。
事務提交時,需要強制Flush(將BufferPool的數(shù)據(jù)落盤)。以此保證Commit標記落盤前,對應事務的所有數(shù)據(jù)落盤,
落盤順序:Log記錄->Data->Commit標記。恢復時可以根據(jù)Commit標記判斷事務的狀態(tài),并通過Undo Log中記錄的舊值將未提交事務的修改回滾。
- Durability of Updates(持久性保證):Data強制刷盤,已經(jīng)Commit的事務由于Data都已經(jīng)在Commit標記前落盤,因此會一直存在;
- Failure Atomic(原子性保證):Undo log內(nèi)容保證,失敗事務的已刷盤的修改會在恢復階段通過Undo log日志回滾,不在可見。
Undo-Only不能解決Page內(nèi)并發(fā)的事務,例如兩個事務的修改落到一個Page中,一個事務提交前需要強制Flush操作,會導致同Page所有事務的Data落盤,可能會早于對應的Log項從而損害WAL,同時也會導致關(guān)鍵路徑上過于頻繁的磁盤隨機訪問。
注:BufferPool的刷盤策略采用的是force;
2. Redo-Only Logging
不同于Undo-Only,采用Redo-Only的Log記錄的是修改后的新值。對應的,Commit需要保證:Log中的Commit標記在事務的任何數(shù)據(jù)之前落盤,即落盤順序:Log記錄->Commit標記->Data?;謴蜁r同樣根據(jù)Commit標記判斷事務狀態(tài),并通過Redo log中新值將已經(jīng)Commit但是沒有落盤的事務修改重放。
- Durability of Updates(持久性保證):Redo log內(nèi)容保證,已提交事務但未刷盤的修改,利用Redo log中的內(nèi)容重放,之后可見;
- Failure Atomic(原子性保證):阻止Commit前Data落盤保證,失敗事務的修改不會出現(xiàn)在磁盤上,自然不可見。
Redo-Only同樣不能有Page內(nèi)并發(fā)問題,Page中多個不同事務,只要有一個未提交就不能刷盤,這些數(shù)據(jù)全部都要維護在內(nèi)存中,造成較大的內(nèi)存壓力。
3. Redo-Undo Logging
可以看出只有Undo或者Redo問題,主要來自對Commit標記以及Data落盤順序的限制,而這種限制來源于:Log信息中對新值或舊值的缺失。因此Redo-Undo采用同時記錄新值和舊值方式,來消除Commit和Data之間刷盤順序的限制。
- Durability of Updates(持久性保證):Redo log內(nèi)容保證,已提交事務但未刷盤的修改,利用Redo log中的內(nèi)容重放,之后可見;
- Failure Atomic(原子性保證):Undo log內(nèi)容保證,失敗事務的已刷盤的修改會在恢復階段通過Undo log日志回滾,不在可見。
如此一來:同Page的不同事務提交會變得非常簡單。同時可以將連續(xù)的數(shù)據(jù)攢著進行批量刷盤已利用磁盤較高的順序讀寫能力。
2.3.3 BufferPool的Force和Steal
從上面看出:Redo和Undo內(nèi)容分別可以保證Durability和Atomic兩個特性,其中一種信息的缺失需要用嚴格的刷盤順序來彌補。這里關(guān)注刷盤順序包含兩個維度:
- Force or No-Force:Commit時是否需要強制刷盤,采用Force的方式由于所有已提交事務數(shù)據(jù)一定已經(jīng)存在于磁盤,自然而然地保證了Durability;
- No-Steal or Steal:Commit前數(shù)據(jù)能否提前刷盤,采用No-Steal的方式由于保證事務提交前修改不會出現(xiàn)在磁盤上,自然而然保證了Atomic。
總結(jié)一下,實現(xiàn)Durability可以通過記錄Redo信息或要求Force刷盤順序,實現(xiàn)Atomic需要記錄Undo信息或要求No-Steal刷盤順序,組合得到如下四種模式,如下圖所示:

2.3.4 ARIES算法
InnoDB采用的ARIES算法,ARIES本質(zhì)是一種Redo-Undo的WAL實現(xiàn)。
Normal過程:
- 修改數(shù)據(jù)之前先追加Log記錄,Log內(nèi)容同時包括Redo和Undo信息,每個日志記錄產(chǎn)生LSN(Log Sequence Number),來標記日志中的位置;
- 數(shù)據(jù)Page記錄最后修改的日志項LSN,以此判斷Page中內(nèi)容的新舊程度,實現(xiàn)冪等。
- 故障恢復階段需要通過Log中的內(nèi)容恢復數(shù)據(jù)庫狀態(tài),為了減少恢復時需要處理的日志量,ARIES會在正常運行期間周期性的生成Checkpoint,Checkpoint中除了當前日志LSN外,還需要記錄當前活躍事務的最新LSN,以及所有臟頁,供恢復時決定重放Redo的開始位置。需要注意的是:由于生成checkpoint時數(shù)據(jù)庫還在正常提供服務(Fuzzy checkpoint),其中記錄的活躍事務以及Dirty Page信息不一定準確,因此需要Recovery階段通過Log內(nèi)容進行修正。
為什么需要checkpoint?
WAL有一個顯著的問題,隨著系統(tǒng)運行時間越長,log會變得越來越長,導致每次crash之后,dbms需要對整個log進行恢復操作,所以dbms定期會做checkpoint,將當前在內(nèi)存中所有數(shù)據(jù)全部刷到磁盤,則下次恢復只需要從最新的checkpoint開始即可。
Recover過程:
故障恢復包括三個階段:Analysis,Redo和Undo。
- Analysis:主要是利用Checkpoint及Log中的信息確認后續(xù)Redo和Undo階段的操作范圍,通過Log修正Checkpoint中記錄的Dirty Page集合信息,并用其中涉及最小的LSN位置作為下一步Redo的開始位置RedoLSN。同時修正Checkpoint中記錄的活躍事務集合(未提交事務),作為Undo過程的回滾對象;
- Redo階段:從Analysis獲得的RedoLSN出發(fā),重放所有的Log中的Redo內(nèi)容,注意這里也包含了未Commit事務;
- Undo階段對所有未提交事務利用Undo信息進行回滾,通過Log的PrevLSN可以順序找到事務所有需要回滾的修改。
2.4 redo log也是寫磁盤,比BufferPool寫入磁盤優(yōu)點是什么?
redo log也需要在事務提交時(默認策略)將日志寫入磁盤,但是它要比Buffer Pool中修改的數(shù)據(jù)寫入磁盤(即刷臟)要快:
- 刷臟是隨機IO,每次修改數(shù)據(jù)位置都是隨機,寫redo log是追加操作,屬于順序IO;
- 刷臟是以數(shù)據(jù)頁(Page)為單位,Mysql默認頁大小為16KB,一個Page上一個小修改都要整頁寫入,而redo log中是精簡的日志數(shù)據(jù),無效IO大大減少。
2.5 redo log刷盤規(guī)則
當事務提交時,需要先將事務日志寫入redo log Buffer中,這些寫入redo log Buffer的日志并不是隨著事務的提交立刻刷新到redo log 文件中,而是有一定的規(guī)則,從而保證了 Redo Log 文件中數(shù)據(jù)的持久性。這種刷盤規(guī)則可以由innodb_flush_log_at_trx_commit變量控制,它的取值:
- 0:每次提交事務時,不會將 Log Buffer 中的日志寫入 OS buffer, 而是通過一個單獨的線程,每秒寫入 OS buffer 并調(diào)用系統(tǒng)的 fsync() 函數(shù)寫入磁盤的 Redo Log File, 這種方式不是實時寫磁盤的, 而是每隔 1s 寫一次日志,如果系統(tǒng)崩潰,可能會丟失 1s 的數(shù)據(jù)。
- 1(默認):每次提交事務都會將 Log Buffer 中的日志寫入 OS buffer 中,并且會調(diào)用 fsync() 函數(shù)將日志寫入 Redo Log File 中,這種方式雖然不會再崩潰時丟失數(shù)據(jù),但是性能比較差。也是這個變量的默認值。
- 2:每次提交事務時,都只是將數(shù)據(jù)寫入 os buffer 中,之后每隔 1s ,通過 fsync() 函數(shù)將 os buffer 中的數(shù)據(jù)寫入 Redo Log 文件中。
2.6 Redo 的整體流程

- 先將原始數(shù)據(jù)從磁盤中讀入到內(nèi)存(Buffer Pool)中,修改內(nèi)存拷貝;
- 生成redo log并寫入
redo log buffer(內(nèi)存),記錄的是數(shù)據(jù)被修改后的值; - 當事務commit時,將redo log中的內(nèi)容刷新到
redo log file(磁盤),對redo log file采用追加寫的方式; - 定期將內(nèi)存(Buffer Pool)中修改的值刷新到磁盤;
2.7 redo log的兩階段提交
redo log 采用是兩階段提交的方式最終commit,那么為什么采用兩階段提交的方式?
看上面的流程圖,mysql在寫redo log兩階段變更時會寫bin log日志表。而記錄binlog日志的目的:既可以用于數(shù)據(jù)恢復、binlog數(shù)據(jù)監(jiān)聽、主從庫同步。那么redo log表采用兩階段提交的目的在于:保證binlog 和redo log文件的一致性。
若不采用兩階段提交:
- 先寫redo log在寫binlog
如果引擎寫完redo log后,bin log還沒有寫。異常重啟。主庫使用redo log 日志將數(shù)據(jù)恢復。但binlog沒有記錄這個語句,那么從庫根據(jù)binlog同步數(shù)據(jù)時依舊沒有這條語句,造成了主從庫的數(shù)據(jù)不一致性;
- 先寫binlog在寫redo log
寫完binlog后異常重啟,因為redo log沒有些,主庫恢復后沒有這條事務。但是由于binlog中有這條記錄,從庫根據(jù)binlog日志同步數(shù)據(jù)時,也會有這條事務。依舊導致主從不一致。
3. redo log 和binlog
3.1 redo log和binlog的區(qū)別
- redo log是InnoDB存儲引擎層面,而binlog是mysql server層面,所有存儲引擎均可使用;
- redo log是InnoDB為了解決
crash safe(系統(tǒng)崩潰后恢復),而binlog是定期存檔,重要的作用是支持主從同步。 - redo log是循環(huán)寫,空間滿時就會發(fā)生寫覆蓋;binlog是追加寫,不會覆蓋。
- bin log屬于邏輯日志,因為沒有涉及在具體哪一個page上進行修改;redo log屬于物理邏輯日志(Physiological Logging),雖然有很多人認為其屬于物理日志。
3.2 為什么redo log具有crash-safe能力,而binlog沒有
redo log:是一個固定大小,“循環(huán)寫”的日志文件,記錄物理邏輯日志;
binlog:是一個無限大小,“追加寫”的日志文件,記錄的是邏輯日志;
redo log只會記錄未刷盤的日志,已經(jīng)刷入磁盤的數(shù)據(jù)都會從redo log這個固定大小的日志文件里刪除;binlog是追加日志,保存的是全量的日志。
當數(shù)據(jù)庫crash崩潰后,想要恢復:未刷盤但已經(jīng)寫入redo log和binlog的數(shù)據(jù)到內(nèi)存時,binlog是無法實現(xiàn)的。雖然binlog擁有全量日志,但是沒有標志讓InnoDB判斷哪些數(shù)據(jù)已經(jīng)刷盤。
但redo log不一樣,只要寫入磁盤的數(shù)據(jù),都會從redo log中抹除,數(shù)據(jù)庫重啟后,直接將redo log的數(shù)據(jù)恢復到內(nèi)存。
3.3 未提交的事務日志也在Redo log中
因為不同事務的日志在Redo log是交叉存在的,所以沒有辦法把未提交的事務與已提交的事務分開。ARIES的做法是:不管事務有沒有提交,它的日志都會被記錄在Redo log上并刷到磁盤中。當崩潰的時候,會把Redo log全部回放一遍,然后再把未提交的事務給找出來,做回滾處理。
4. 寫磁盤flush操作
InnoDB執(zhí)行update更新操作是采用的“先寫日志,在寫磁盤”的策略。更新后的行數(shù)據(jù)本身先緩存在內(nèi)存中,直將縮略的關(guān)鍵信息寫入到redo log磁盤。但緩存在內(nèi)存中的數(shù)據(jù)最終總是要寫入到磁盤,這個操作叫做flush。
當內(nèi)存數(shù)據(jù)頁和磁盤數(shù)據(jù)頁不一致的時候,稱這個內(nèi)存頁為“臟頁”。內(nèi)存數(shù)據(jù)寫入到磁盤后,內(nèi)存和磁盤上的數(shù)據(jù)頁的內(nèi)容就一致了,稱為“干凈頁”。flush操作也就是“刷臟頁”。
4.1 觸發(fā)數(shù)據(jù)庫執(zhí)行flush操作的四種情況:
4.1.1 當InnoDB的redo log寫滿時
此時系統(tǒng)會停止所有的更新操作,將環(huán)形的redo log中的“讀指針”向前推,對應的所有臟頁此時都會flush到磁盤上。
4.1.2 當系統(tǒng)的內(nèi)存不足時
當需要新的內(nèi)存頁,但是內(nèi)存不夠用時,就需要淘汰一些內(nèi)存頁(一般是空出最長時間沒有被訪問的內(nèi)存頁),此時如果淘汰的是臟頁,就需要先將臟頁寫到磁盤。
4.1.3 當mysql認為系統(tǒng)“空閑”時
mysql會在運行期間“見縫插針”的找機會刷一點臟頁,以避免當讀寫業(yè)務繁忙時過快的占滿系統(tǒng)內(nèi)存或redo log空間。
4.1.4 當mysql正常關(guān)閉時
此時mysql會把內(nèi)存中所有臟頁都flush到磁盤上,這樣mysql下次啟動的時候就直接從磁盤上讀取數(shù)據(jù),啟動速度更快(相比即從磁盤讀取數(shù)據(jù),又從磁盤讀取redo log日志)。