硬核干貨!一文掌握MySQL核心日志:binlog 、redo log、undo log
在MySQL 中我們經(jīng)常會(huì)接觸到三個(gè)核心日志,它們分別是:binlog、redo log、undo log。
好多同學(xué)對(duì)于它們可能并不陌生,但是具體區(qū)分起來(lái)各自的功能用途以及實(shí)現(xiàn)原理,那可能認(rèn)知就會(huì)比較模糊了,今天就跟大家一起,來(lái)清晰明了的介紹一下這些日志的核心思想和功能原理。
圖片
1 binlog
1.1 binlog 設(shè)計(jì)目標(biāo)
binlog 記錄了對(duì)MySQL數(shù)據(jù)庫(kù)執(zhí)行更改的所有的寫操作,包括所有對(duì)數(shù)據(jù)庫(kù)的數(shù)據(jù)、表結(jié)構(gòu)、索引等等變更的操作。
注意:這其中不包含SELECT、SHOW等,因?yàn)閷?duì)數(shù)據(jù)沒(méi)有修改
只要是對(duì)數(shù)據(jù)庫(kù)有變更的操作都會(huì)記錄到binlog里面來(lái),我們可以把數(shù)據(jù)庫(kù)的數(shù)據(jù)看做銀行賬戶里的余額,而binlog就相當(dāng)于我們銀行卡的流水記錄。賬戶余額只是一個(gè)結(jié)果,至于這個(gè)結(jié)果怎么來(lái)的,那就必須得看流水了。
在實(shí)際應(yīng)用中, binlog 的主要應(yīng)用場(chǎng)景分別是 主從復(fù)制 和 數(shù)據(jù)恢復(fù)。
- 主從復(fù)制 :在 Master 端開啟 binlog ,然后將 binlog 發(fā)送到各個(gè) Slave 端, Slave 端重放 binlog 來(lái)達(dá)到主從數(shù)據(jù)一致。
- 數(shù)據(jù)恢復(fù) :通過(guò)使用 mysqlbinlog 工具來(lái)恢復(fù)數(shù)據(jù)。
圖片
1.2 binlog 數(shù)據(jù)格式
binlog 日志有三種格式,分別為 STATMENT 、 ROW 和 MIXED。
在 MySQL 5.7.7 之前,默認(rèn)的格式是 STATEMENT , MySQL 5.7.7 之后,默認(rèn)值是 ROW。日志格式通過(guò) binlog-format 指定。
- ROW:基于行的復(fù)制(row-based replication, RBR),不記錄每條SQL語(yǔ)句的上下文信息,僅需記錄哪條數(shù)據(jù)被修改了。如果一個(gè)update語(yǔ)句修改一百行數(shù)據(jù),那么這種模式下就會(huì)記錄100行對(duì)應(yīng)的記錄日志。
優(yōu)點(diǎn):不會(huì)出現(xiàn)某些特定情況下的存儲(chǔ)過(guò)程、或function、或trigger的調(diào)用和觸發(fā)無(wú)法被正確復(fù)制的問(wèn)題;
缺點(diǎn):會(huì)產(chǎn)生大量的日志,尤其是 alter table 的時(shí)候會(huì)讓日志暴漲。
- STATMENT:基于SQL語(yǔ)句的復(fù)制( statement-based replication, SBR ),每一條會(huì)修改數(shù)據(jù)的SQL語(yǔ)句會(huì)記錄到 binlog 中 。相對(duì)于ROW模式,STATEMENT模式下只會(huì)記錄這個(gè) update 的語(yǔ)句,所以此模式下會(huì)非常節(jié)省日志空間,也避免著大量的IO操作。
優(yōu)點(diǎn):不需要記錄每一行的變化,減少了 binlog 日志量,節(jié)約了 IO , 從而提高了性能;
缺點(diǎn):在某些情況下會(huì)導(dǎo)致主從數(shù)據(jù)不一致,比如執(zhí)行sysdate() 、 slepp() 等 。
- MIXED:基于 STATMENT 和 ROW 兩種模式的混合復(fù)制(mixed-based replication, MBR),一般的復(fù)制使用 STATEMENT 模式保存 binlog ,對(duì)于一些函數(shù),STATEMENT 模式無(wú)法復(fù)制的操作使用 ROW 模式保存 binlog。
基于這三種模式需要注意的是:
1)使用 row 格式的 binlog 時(shí),在進(jìn)行數(shù)據(jù)同步或恢復(fù)的時(shí)候不一致的問(wèn)題更容易被發(fā)現(xiàn),因?yàn)樗腔跀?shù)據(jù)行記錄的。
2)使用 mixed 或者 statement 格式的 binlog 時(shí),很多事務(wù)操作都是基于SQL邏輯記錄,我們都知道一個(gè)SQL在不同的時(shí)間點(diǎn)執(zhí)行它們產(chǎn)生的數(shù)據(jù)變化和影響是不一樣的,所以這種情況下,數(shù)據(jù)同步或恢復(fù)的時(shí)候就容易出現(xiàn)不一致的情況。
1.3 binlog 寫入策略
對(duì)于 InnoDB 存儲(chǔ)引擎而言,在進(jìn)行事務(wù)的過(guò)程中,首先會(huì)把binlog 寫入到binlog cache中(因?yàn)閷懭氲絚ache中會(huì)比較快,一個(gè)事務(wù)通常會(huì)有多個(gè)操作,避免每個(gè)操作都直接寫磁盤導(dǎo)致性能降低),只有在事務(wù)提交時(shí)才會(huì)記錄 biglog ,此時(shí)記錄還在內(nèi)存中,那么 biglog 是什么時(shí)候刷到磁盤中的呢?
MySQL 其實(shí)是通過(guò) sync_binlog 參數(shù)控制 biglog 的刷盤時(shí)機(jī),取值范圍是 0-N:
- 0:每次提交事務(wù)binlog不會(huì)馬上寫入到磁盤,而是先寫到page cache。不去強(qiáng)制要求,由系統(tǒng)自行判斷何時(shí)寫入磁盤,在Mysql 崩潰的時(shí)候會(huì)有丟失日志的風(fēng)險(xiǎn);
- 1:每次提交事務(wù)都會(huì)執(zhí)行 fsync 將 binlog 寫入到磁盤;
- N:每次提交事務(wù)都先寫到page cach,只有等到積累了N個(gè)事務(wù)之后才 fsync 將 binlog 寫入到磁盤,在 MySQL 崩潰的時(shí)候會(huì)有丟失N個(gè)事務(wù)日志的風(fēng)險(xiǎn)。
很顯然三種模式下,sync_binlog=1 是強(qiáng)一致的選擇,選擇0或者N的情況下在極端情況下就會(huì)有丟失日志的風(fēng)險(xiǎn),具體選擇什么模式還是得看系統(tǒng)對(duì)于一致性的要求。
2、redo log
2.1 redo log 設(shè)計(jì)目標(biāo)
redo log 是屬于引擎層(innodb)的日志,稱為重做日志 ,當(dāng)MySQL服務(wù)器意外崩潰或者宕機(jī)后,保證已經(jīng)提交的事務(wù)持久化到磁盤中(持久性)。
它能保證對(duì)于已經(jīng)COMMIT的事務(wù)產(chǎn)生的數(shù)據(jù)變更,即使是系統(tǒng)宕機(jī)崩潰也可以通過(guò)它來(lái)進(jìn)行數(shù)據(jù)重做,達(dá)到數(shù)據(jù)的持久性,一旦事務(wù)成功提交后,不會(huì)因?yàn)楫惓!㈠礄C(jī)而造成數(shù)據(jù)錯(cuò)誤或丟失。
圖片
2.2 redo log 數(shù)據(jù)格式
redo log 包括兩部分:
- 內(nèi)存中的日志緩沖(redo log buffer)
- 內(nèi)存層面,默認(rèn)16M,通過(guò)innodb_log_buffer_size參數(shù)可修改
- 磁盤上的日志文件(redo logfile)
- 持久化的,磁盤層面
MySQL 每執(zhí)行一條 DML 語(yǔ)句,先將記錄寫入 redo log buffer,后續(xù)某個(gè)時(shí)間點(diǎn)再一次性將多個(gè)操作記錄寫到 redo log file。
通常所說(shuō)的Write-Ahead Log(預(yù)先日志持久化)指的是在持久化一個(gè)數(shù)據(jù)頁(yè)之前,先將內(nèi)存中相應(yīng)的日志頁(yè)持久化。
在計(jì)算機(jī)操作系統(tǒng)中,用戶空間( user space )下的緩沖區(qū)數(shù)據(jù)一般情況下是無(wú)法直接寫入磁盤的,中間必須經(jīng)過(guò)操作系統(tǒng)內(nèi)核空間( kernel space )緩沖區(qū)( OS Buffer )。
因此, redo log buffer 寫入 redo logfile 實(shí)際上是先寫入 OS Buffer ,然后再通過(guò)系統(tǒng)調(diào)用 fsync() 將其刷到 redo log file中,過(guò)程如下:
圖片
修改數(shù)據(jù)的操作流程:
圖片
- 先將原始數(shù)據(jù)從磁盤中讀入內(nèi)存中來(lái),修改數(shù)據(jù)的內(nèi)存拷貝,產(chǎn)生臟數(shù)據(jù)
- 生成一條重做日志并寫入redo log buffer,記錄的是數(shù)據(jù)被修改后的值
- 默認(rèn)在事務(wù)提交后將redo log buffer中的內(nèi)容刷新到redo log file,對(duì)redo log file采用追加寫的方式
- 定期將內(nèi)存中修改的數(shù)據(jù)刷新到磁盤中(這里說(shuō)的是那些還沒(méi)及時(shí)被后臺(tái)線程刷盤的臟數(shù)據(jù))
2.3 關(guān)于 redo log 的幾點(diǎn)疑惑
讀到這里,相必有同學(xué)會(huì)有如下疑問(wèn):
Q1:為什么不直接修改磁盤中的數(shù)據(jù)?
因?yàn)橹苯有薷拇疟P數(shù)據(jù)的話,它是隨機(jī)IO,修改的數(shù)據(jù)分布在磁盤中不同的位置,需要來(lái)回的查找,所以命中率低,消耗大,而且一個(gè)小小的修改就不得不將整個(gè)頁(yè)刷新到磁盤,利用率低;
與之相對(duì)的是順序IO,磁盤的數(shù)據(jù)分布在磁盤的一塊,所以省去了查找的過(guò)程,節(jié)省尋道時(shí)間。
使用后臺(tái)線程以一定的頻率去刷新磁盤可以降低隨機(jī)IO的頻率,增加吞吐量,這是使用buffer pool的根本原因。
Q2:同為操作數(shù)據(jù)變更的日志,有了binlog為什么還要redo log?
我認(rèn)為最核心的一點(diǎn)就是兩者記錄的數(shù)據(jù)變更粒度是不一樣的。
以修改數(shù)據(jù)為例,binlog 是以表為記錄主體,在ROW模式下,binlog保存的表的每行變更記錄。
MySQL 是以頁(yè)為單位進(jìn)行刷盤的,每一頁(yè)的數(shù)據(jù)單位為16K,所以在刷盤的過(guò)程中需要把數(shù)據(jù)刷新到磁盤的多個(gè)扇區(qū)中去。而把16K數(shù)據(jù)刷到磁盤的每個(gè)扇區(qū)里這個(gè)過(guò)程是無(wú)法保證原子性的,如果數(shù)據(jù)庫(kù)宕機(jī),那么就可能會(huì)造成一部分?jǐn)?shù)據(jù)成功,而一部分?jǐn)?shù)據(jù)失敗的情況。而通過(guò) binlog 這種級(jí)別的日志是無(wú)法恢復(fù)的,因?yàn)橐粋€(gè)update可能更改了多個(gè)磁盤區(qū)域的數(shù)據(jù),所以這個(gè)時(shí)候得需要通過(guò)redo log這種記錄到磁盤數(shù)據(jù)級(jí)別的日志進(jìn)行數(shù)據(jù)恢復(fù)。
圖片
由以上兩者的對(duì)比可知:binlog 日志只用于歸檔,只依靠 binlog 是沒(méi)有 crash-safe 能力的。
同樣只有 redo log 也不行,因?yàn)?redo log 是 InnoDB特有的,且日志上的記錄落盤后會(huì)被覆蓋掉。因此需要 binlog和 redo log二者同時(shí)記錄,才能保證當(dāng)數(shù)據(jù)庫(kù)發(fā)生宕機(jī)重啟時(shí),數(shù)據(jù)不會(huì)丟失。
Q3:redo log一定能保證事務(wù)的持久性嗎?
不一定,這要根據(jù)redo log的刷盤策略決定,因?yàn)閞edo log buffer同樣是在內(nèi)存中,如果提交事務(wù)之后,redo log buffer還沒(méi)來(lái)得及將數(shù)據(jù)刷新到redo log file進(jìn)行持久化,此時(shí)發(fā)生宕機(jī)照樣會(huì)丟失數(shù)據(jù)。
那該如何解決呢?刷盤寫入策略。
2.4 redo log 寫入策略
當(dāng)redo log空間滿了之后又會(huì)從頭開始以循環(huán)的方式進(jìn)行覆蓋式的寫入。MySQL 支持三種將 redo log buffer 寫入 redo log file 的時(shí)機(jī),可以通過(guò) innodb_flush_log_at_trx_commit 參數(shù)配置,各參數(shù)含義如下:
- 0(延遲寫):表示每次事務(wù)提交時(shí)都只是把 redo log 留在 redo log buffer 中,開啟一個(gè)后臺(tái)線程,每1s刷新一次到磁盤中 ;
- 1(實(shí)時(shí)寫,實(shí)時(shí)刷):表示每次事務(wù)提交時(shí)都將 redo log 直接持久化到磁盤,真正保證數(shù)據(jù)的持久性;
- 2(實(shí)時(shí)寫,延遲刷):表示每次事務(wù)提交時(shí)都只是把 redo log 寫到 page cache,具體的刷盤時(shí)機(jī)不確定。
除了上面幾種機(jī)制外,還有其它兩種情況會(huì)把redo log buffer中的日志刷到磁盤。
- 定時(shí)處理:有線程會(huì)定時(shí)(每隔 1 秒)把redo log buffer中的數(shù)據(jù)刷盤。
- 根據(jù)空間處理:redo log buffer 占用到了一定程度( innodb_log_buffer_size 設(shè)置的值一半)占,這個(gè)時(shí)候也會(huì)把redo log buffer中的數(shù)據(jù)刷盤。
3、undo log
3.1 undo log設(shè)計(jì)目標(biāo)
redo log 是也屬于引擎層(innodb)的日志,從上面的redo log介紹中我們就已經(jīng)知道了,redo log 和undo log的核心是為了保證innodb事務(wù)機(jī)制中的持久性和原子性,事務(wù)提交成功由redo log保證數(shù)據(jù)持久性,而事務(wù)可以進(jìn)行回滾從而保證事務(wù)操作原子性則是通過(guò)undo log 來(lái)保證的。
原子性 是指對(duì)數(shù)據(jù)庫(kù)的一系列操作,要么全部成功,要么全部失敗,不可能出現(xiàn)部分成功的情況。
undo log 的主要應(yīng)用場(chǎng)景分別:
- 事務(wù)回滾 :前面提到過(guò),后臺(tái)線程會(huì)不定時(shí)的去刷新buffer pool中的數(shù)據(jù)到磁盤,但是如果該事務(wù)執(zhí)行期間出現(xiàn)各種錯(cuò)誤(宕機(jī))或者執(zhí)行rollback語(yǔ)句,那么前面刷進(jìn)去的操作都是需要回滾的,保證原子性,undo log就是提供事務(wù)回滾的。
- MVCC:當(dāng)讀取的某一行被其他事務(wù)鎖定時(shí),可以從undo log中分析出該行記錄以前的數(shù)據(jù)版本是怎樣的,從而讓用戶能夠讀取到當(dāng)前事務(wù)操作之前的數(shù)據(jù)——快照讀。
3.2 undo log 數(shù)據(jù)格式
undo log 數(shù)據(jù)主要分兩類:
- insert undo log
insert 操作的記錄,只對(duì)事務(wù)本身可見,對(duì)其他事務(wù)不可見(這是事務(wù)隔離性的要求),故該undo log可以在事務(wù)提交后直接刪除,不需要進(jìn)行purge操作。
- update undo log
update undo log記錄的是對(duì)delete和update操作產(chǎn)生的undo log。該undo log可能需要提供MVCC機(jī)制,因此不能在事務(wù)提交時(shí)就進(jìn)行刪除。提交時(shí)放入undo log鏈表,等待purge線程進(jìn)行最后的刪除。
在InnoDB存儲(chǔ)引擎中,undo log使用rollback segment回滾段進(jìn)行存儲(chǔ),每隔回滾段包含了1024個(gè)undo log segment。MySQL5.5之后,一共有128個(gè)回滾段。即總共可以記錄128 * 1024個(gè)undo操作。
每個(gè)事務(wù)只會(huì)使用一個(gè)回滾段,一個(gè)回滾段在同一時(shí)刻可能會(huì)服務(wù)于多個(gè)事務(wù)。
3.3 undo log 操作實(shí)例
1)首先準(zhǔn)備一張?jiān)荚紨?shù)據(jù)表(user_info)
對(duì)于InnoDB引擎來(lái)說(shuō),每個(gè)行記錄除了記錄本身的數(shù)據(jù)之外,還有幾個(gè)隱藏的列:
- DB_ROW_ID∶記錄的主鍵id。
- DB_TRX_ID:事務(wù)ID,當(dāng)對(duì)某條記錄發(fā)生修改時(shí),就會(huì)將這個(gè)事務(wù)的Id記錄其中。
- DB_ROLL_PTR︰回滾指針,版本鏈中的指針。
圖片
2)開啟一個(gè)事務(wù)A
對(duì) user_info 表執(zhí)行如下SQL:
update user_info set name =“李四”where id=1將會(huì)進(jìn)行如下流程操作:
- 首先獲得一個(gè)事務(wù)編號(hào) 104
- 把user_info表修改前的數(shù)據(jù)拷貝到undo log
- 修改user_info表 id=1的數(shù)據(jù)
- 把修改后的數(shù)據(jù)事務(wù)版本號(hào)改成 當(dāng)前事務(wù)版本號(hào),并把DB_ROLL_PTR 地址指向undo log數(shù)據(jù)地址。
3)最后執(zhí)行結(jié)束
結(jié)果如下所示:

可以發(fā)現(xiàn)每次對(duì)數(shù)據(jù)的變更都會(huì)產(chǎn)生一個(gè)undo log,當(dāng)一條記錄被變更多次時(shí),那么就會(huì)產(chǎn)生多條undo log,undo log記錄的是變更前的日志,并且每個(gè)undo log的序號(hào)是遞增的,那么當(dāng)要回滾的時(shí)候,按照序號(hào)依次向前推,就可以找到我們的原始數(shù)據(jù)了。
總結(jié)
binlog 是MySQL server層的日志,而redo log 和undo log都是引擎層(InnoDB)的日志,要換其他數(shù)據(jù)引擎那么就未必有redo log和undo log了。
它的設(shè)計(jì)目標(biāo)是支持innodb的“事務(wù)”的特性,事務(wù)ACID特性分別是原子性、一致性、隔離性、持久性, 一致性是事務(wù)的最終追求的目標(biāo),隔離性、原子性、持久性是達(dá)成一致性目標(biāo)的手段,根據(jù)的之前的介紹我們已經(jīng)知道隔離性是通過(guò)鎖機(jī)制來(lái)實(shí)現(xiàn)的,而事務(wù)的原子性和持久性則是通過(guò)redo log 和undo log來(lái)保障的。
寫入策略
事務(wù)執(zhí)行過(guò)程中,先把日志寫到bin log cache ,事務(wù)提交的時(shí)候,再把binlog cache寫到binlog文件中。因?yàn)橐粋€(gè)事務(wù)的binlog不能被拆開,無(wú)論這個(gè)事務(wù)多大,也要確保一次性寫入,所以系統(tǒng)會(huì)給每個(gè)線程分配一個(gè)塊內(nèi)存作為binlog cache。
圖片
binlog vs redo log
- redo log 物理日志:記錄內(nèi)容是“在xx數(shù)據(jù)頁(yè)做了xx修改”,屬于InnoDB存儲(chǔ)引擎層產(chǎn)生的。
- binlog 邏輯日志:記錄內(nèi)容是語(yǔ)句的原始邏輯,類似于給ID=2這一行的c字段加1,屬于服務(wù)層。
兩個(gè)側(cè)重點(diǎn)也不同, redo log讓InnoDB有了崩潰恢復(fù)的能力,binlog保證了MySQL集群架構(gòu)的數(shù)據(jù)一致性。
圖片
在執(zhí)行更新語(yǔ)句過(guò)程,會(huì)記錄redo log與binlog兩塊日志,以基本的事務(wù)為單位,redo log在事務(wù)執(zhí)行過(guò)程中可以不斷寫入,而binlog只有在提交事務(wù)時(shí)才寫入,所以redo log與binlog的寫入時(shí)機(jī)不一樣。
































