轉發https://segmentfault.com/a/1190000041758784
一、MySQL日誌
MySQL
日誌主要包括錯誤日誌、查詢日誌、慢查詢日誌、事務日誌、二進制日誌幾大類。其中比較重要的就是二進制日誌binlog
(歸檔日誌)、事務日誌redo log
(重做日誌)和undo log
(回滾日誌)。
日誌關係如下圖:
二、redo log
redo log
(重做日誌)是InnoDB
存儲引擎獨有的,它讓MySQL
有了崩潰恢復的能力。
當MySQL
實例掛了或者宕機了,重啓的時候InnoDB
存儲引擎會使用rede log
日誌恢復數據,保證事務的持久性和完整性。如下圖:
MySQL
中數據是以頁爲單數存儲,當你查詢一條記錄時,硬盤會把一整頁的數據加載出來,加載出來的數據叫做數據頁,會放到Buffer Pool
中。後續的查詢都是先從Buffer Pool
中找,沒有找到再去硬盤加載其他的數據頁直到命中,這樣子可以減少磁盤IO
的次數,提高性能。更新數據的時候也是一樣,優先去Buffer Pool
中找
,如果存在需要更新的數據就直接更新。然後會把“在某個數據頁做了什麼修改”記錄到重做日誌緩存(redo log buffer
)裏,在刷盤的時候會寫入redo log
日誌文件裏。
如下圖:
小貼士:每條redo記錄由“表空間號+數據頁號+偏移量+修改數據長度+具體修改的數據”組成。
刷盤時機
理想情況下,事務一提交就會進行刷盤操作,但是實際上是刷盤的時機是根據策略來決定的。
InnoDB
存儲引擎爲redo log
的刷盤策略提供了innodb_flush_log_at_trx_commit
參數,它支持三種策略:
- 0:設置爲0的時候,每次提交事務時不刷盤。
- 1:設置爲1的時候,每次提交事務時刷盤。
- 2:設置爲2的時候,每次提交事務時都只把
redo log buffer
寫入page cache
。
innodb_flush_log_at_trx_commit
參數默認爲1,當事務提交的時候會調用fsync
對redo log
進行刷盤,將redo log buffer
寫入redo log
文件中。
另外,Innodb
存儲引擎有一個後臺線程,每隔1
秒,就會把會redo log buffer
中的內容寫入到文件系統緩存page cache
,然後調用fsync
刷盤。
如上圖,所以說一個沒有提交事務的redo log
記錄,也會被刷盤。
下面是各種刷盤策略的流程圖。
innodb_flush_log_at_trx_commit = 0
如上圖,如果宕機了或者MySQL
掛了可能造成1
秒內的數據丟失。
innodb_flush_log_at_trx_commit = 1
如上圖,只要事務提交成功,redo log
記錄就一定在磁盤裏,不會有任務數據丟失。
如果執行事務的時候MySQL
掛了或者宕機了,這部分日誌丟失了,但是因爲事務沒有提交,所以日誌丟了也不會有損失。
innodb_flush_log_at_trx_commit = 2
如上圖,當事務提交成功時,redo log buffer
日誌會被寫入page cache
,然後後臺線程會刷盤寫入redo log
,由於後臺線程是1
秒執行一次所以宕機或者MySQL
掛了可能造成1
秒內的數據丟失。
日誌文件組
硬盤上存儲的redo log
日誌文件不止一個,而是一個日誌文件組的形式出現的,每個的redo log
文件大小都是一樣的。它採用的是環形數組形式,從頭開始寫,寫到末尾回到頭循環寫,如下圖所示:
在日誌文件組中有兩個重要的屬性,分別是witre pos、checkpoint
- wirte pos:是當前記錄的位置,一邊寫一邊後移。
- checkpoint:是當前要擦除的位置,也是後臺推移。
每次刷盤redo log
記錄到日誌文件組中,wirte log
位置就會後移更新。
每次MySQL
加載日誌文件組恢復數據時,會清空加載過的redo log
,並把checkpoint
後移更新。
write pos
和 checkpoint
之間的還空着的部分可以用來寫入新的 redo log
記錄。
如果 witre pos
追上checkpoint
,表示日誌文件組滿了,這時候不能再寫入新的redo log
記錄,MySQL
得停下來,清空一些記錄,把checkpoint
推薦一下。
redo log小結
redo log
的作用和它的刷盤時機、存儲形式。
可以思考一個問題:只要每次把修改後的數據頁直接刷盤不就好了,爲什麼還要用redo log
刷盤?不都是刷盤嗎?有什麼區別?
實際上,數據頁大小是16KB
,刷盤比較耗時,可能就修改了數據頁的幾byte
數據,沒有必要把整頁的數據刷盤。而且數據頁刷盤都是隨機寫,因爲一個數據頁對應的位置可能是在硬盤文件的隨機位置,所以性能很差。
如果是寫redo log
,一行記錄就佔了幾十byte
,只要包含了表空間號、數據頁號、磁盤文件偏移量、修改值,再加上是順序寫,所以刷盤效率很高。
所以用 redo log
形式記錄修改內容,性能會遠遠超過刷數據頁的方式,這也讓數據庫的併發能力更強。
三、binlog
redo log
是物理日誌,記錄的是“在某個數據頁做了什麼修改”,屬於Innodb
存儲引擎。
而binlog
日誌是邏輯日誌,記錄內容是語句的原始邏輯,屬於MySQL Server
層。所有的存儲引擎只要發生了數據更新,都會產生binlog
日誌。
binlog
日誌的作用
可以說MySQL
數據庫的數據備份、主備、主主、住從都離不開binlog
,需要依賴binlog
來同步數據,保證數據一致性。
binlog
會記錄所有涉及更新數據的邏輯規則,並且按順序寫。
記錄格式
binlog
日誌有三種格式,可以通過binlog_format
參數設置,有以下三種:
- statement
- row
- mixed
設置statement
記錄的內容是SQL
語句原文,比如執行一條update T set update_time = now() where id = 1
,記錄內容如下:
同步數據時,會執行記錄的SQL
語句,但是有個問題update_time = now()
這裏會獲取到當前系統問題,直接執行會導致與原庫數據不一致。
爲了解決這種問題,我們需要將binlog_format
設置成row
,記錄的不再是簡單的SQL
語句了,還包含了操作的具體數據,記錄內容如下:
row
格式記錄的內容看不到詳細信息,通過mysqlbinlog
工具解析出來。
update_time = now()
變成了具體的時間,條件後面的@1、@2
都是該行數據第1個~2個字段的原始值(假設這張表只有2個字段)。
設置成row
帶來的好處就是同步數據的一致性,通常情況都設置成row
,這樣可以爲數據庫的恢復與同步帶來更好的可靠性。但是這種格式需要大量的容量來記錄,比較佔用空間,恢復與同步時會更消耗IO
資源,影響執行速度。
所以又有了一種折中方案,設置爲mixed
,記錄的內容是前兩者的混合。
MySQL
會判斷這條SQL
語句是否會引起數據不一致,如果是就用row
格式,否則就用statement
格式。
寫入機制
binlog
的寫入時機爲事務執行過程中,先把日誌寫到binlog cache
,事務提交的時候再把binlog cache
寫到binlog
文件中(實際先會寫入page cache
,然後再由fsync
寫入binlog
文件)。
因爲一個事務的binlog
不能被拆開,無論這個事務多大,也要確保一次性寫入,所以系統會給每個線程分配一塊內存作爲binlog cache
。可以通過binlog_cache_size
參數控制單線程binlog_cache
大小,如果存儲內容超過了這個參數,就要暫存到磁盤。
binlog
日誌刷盤流程如下:
- 上圖的
write
,是指把日誌寫入到文件系統的page cache
,並沒有把數據持久化硬盤,所以速度比較快。 - 上圖的
fsync
纔是將數據庫持久化到硬盤的操作。
write
和fsync
的時機可以由參數sync_binlog
控制,可以配置成0、1、N(N>1)
。
- 設置成0時:表示每次提交事務都只會
write
,由系統自行判斷什麼時候執行fsync
。 - 設置成1時:表示每次提交事務都會執行
fsync
,就和redo log
日誌刷盤流程一樣。 - 設置成N時:表示每次提交事務都會
write
,但是積累N
個事務後才fsync
。
設置爲0時如下圖:
從上圖可知,sync_bilog = 0
設置成0
,只把日誌寫入page cache
雖然性能得到了提高,但是事務提交了fsync
的時候宕機了,可能造成binlog
日誌的丟失。
設置爲2時如下圖:
在出現IO
瓶頸的場景裏,將sync_binlog
設置成一個比較大的值,可以提升性能。
同樣的,如果機器宕機,會丟失最近N
個事務的binlog
日誌。
兩階段提交
redo log
(重做日誌)讓InnoDB
存儲引擎有了崩潰恢復的能力。
binlog
(歸檔日誌)保證了MySQL
集羣架構數據的一致性。
雖然它們都屬於持久化的保證,但是側重點不一樣。
在執行更新語句過程,會記錄redo log
與binlog
兩塊日誌,以基本的事務爲單位,redo log
在事務執行過程中可以不斷寫入,而binlog
日誌只有在提交事務的時候纔會寫入,所以它們寫入的時機不一樣。
思考一個問題,如果redo log和binlog兩份日誌之間的邏輯不一樣,會出現什麼問題呢?MySQL是怎麼解決這個問題的呢?
比如有這樣一個場景,假設有這麼一條語句update T set c = 1 where id = 2
(c原值爲0),假如執行過程中寫完redo log
日誌後,在寫入binlog
的時候發生了異常,會出現什麼情況呢?
如下圖:
由於binlog
日誌沒寫完就異常,這個時候binlog
日誌裏面沒有對應的修改記錄,之後使用binlog
同步的數據的時候就會少這一次的更新,這一行數據c = 0
,而原庫使用redo log
日誌恢復,這一行數據c = 1
,最終數據不一致。如下圖:
爲了解決兩份日誌之間的邏輯不一致的問題,InnoDB
存儲引擎使用兩階段提交方案。
將redo log
日誌的寫入拆分成兩個步驟prepare
和commit
,如下圖:
使用兩階段提交後,寫入binlog
時發生異常也沒關係,因爲MySQL
根據redo log
日誌恢復數據時,發現redo log
日誌處於prepare
階段,並且沒有對應binlog
日誌(根據事務id對應),所以就會回滾事務。
再想一個場景,redo lgo
設置commit
階段發生異常,事務會不會回滾呢?
並不會回滾事務,雖然redo log
是處於prepare
階段,但是存在對應的事務binlog
日誌,所以MySQL
認爲是完整的,所以不會回滾事務。
undo log
想要保證事務的原子性,就需要在發生異常時,對已經執行的操作進行回滾,在MySQL
中恢復機制是通過undo log
(回滾日誌)實現的,所有事務進行的修改都會先被記錄到這個回滾日誌,然後再執行其他相關的操作。如果執行過程中遇到異常的話,我們直接利用回滾日誌中的信息將數據回滾到修改之前的樣子。並且,回滾日誌會先於數據持久化到磁盤上。這樣就保證了即使遇到數據庫突然宕機等情況,當用戶再次啓動數據庫的時候,數據庫還能夠通過查詢回滾日誌來回滾將之前未完成的事務。
另外,MVCC
的實現依賴:隱藏字段、Read View
、undo log
。在底層實現中,InnoDB
通過數據行的DB_TRX_ID
和Read View
來判斷數據的可見性,如不可見,則通過數據行DB_ROLL_PTR
找到undo log
中的歷史版本。每個事務讀到的數據版本可能是不一樣的,在同一個事物裏,用戶只能看到該事務創建Read View
之前已經提交的修改和該事務本身做的修改。
總結
MySQL InnoDB
引擎使用redo log
日誌保證事務的持久性,使用undo log
日誌保證事務的原子性。
MySQL
數據庫的數據備份、主備、主主、主從離不開binlog
,需要依賴binlog
來同步數據,保證數據的一致性。