BinLog是MySQL Server層的日志,所有的MySQL存盤引擎都支持BinLog,BinLog可以支持主從復制和資料恢復,但是對事務的ACID特性支持比較差,InnoDB存盤引擎引入RedoLog和UndoLog事務日志,用于提升事務場景下的資料庫性能,本文會對RedoLog和UndoLog進行介紹,
RedoLog和UndoLog
ChangeBuffer和WAL
我們以一條SQL更新陳述句來介紹RedoLog的作用,首先在資料庫中創建user_info表,該表包含主鍵列id和姓名列,并向資料庫中插入一列測驗資料:
create table user_info
(
id int primary key,
name varchar(255)
);
insert into user_info(id,name) value (1,'ls');
查詢陳述句的執行流程
如果我們需要查詢id=1的用戶的資訊,我們可以通過以下SQL陳述句進行查詢:
select * from user_info where id = 1;
在這一條簡單的查詢陳述句之后,MySQL做了哪些作業呢?如下所示,MySQL執行SQL查詢陳述句的流程包含以下步驟:
- 連接器:客戶端和MySQL服務端建立連接,用戶名密碼等資訊校驗;
- 查詢快取:如果SQL陳述句是查詢陳述句,則查看查詢陳述句是否命中快取;
- 分析器:對SQL陳述句的詞法和語法進行分析,判斷SQL陳述句的型別和對應的表等資訊;
- 優化器:對SQL陳述句進行優化,選擇合適的索引;
- 執行器:在對應的MySQL引擎上執行SQL查詢陳述句,并回傳查詢結果;

更新陳述句的執行流程
如果我們不需要查詢用戶資訊,而是要更新id=1的記錄中的用戶名為zs,則可以通過以下SQL陳述句進行更新:
update user_info set name="zs" where id=1;
和上文中的查詢陳述句類似,MySQL一樣會先通過連接器建立資料庫連接,然后通過分析器、優化器和執行器查找到需要更新的資料所在的行,然后更新資料,
和查詢流程不一樣的是,更新流程還涉及ChangeBuffer和兩個重要的日志模塊:BinLog和RedoLog,其中BinLog和ChangeBuffer的作用已經在前文中介紹過,BinLog用于主從復制和資料恢復,ChangeBuffer用于快取對資料庫中資料的操作,RedoLog則是本文介紹的主角了,
ChangeBuffer技術
對于上文中的更新陳述句,如果沒有RedoLog,那么InnoDB引擎會按照索引查找到id=1的用戶記錄,把記錄加載到記憶體中,然后修改記憶體中的資料事務提交后再寫回磁盤,如果資料庫資料更新的頻率非常低,那么這樣更新方式資料庫也可以接受,但是在更新非常頻繁的情況下,大量的離散IO會成為資料庫的瓶頸,影響資料庫的性能,

在更新頻繁的場景下,如何降低磁盤的IO并保證事務呢?這就涉及到我們前邊文章中介紹過的ChangeBuffer技術了,在滿足ChangeBuffer快取操作的條件下,InnoDB并不會立即把資料的變更操作寫入磁盤,而是將這些對資料頁的操作快取到ChangeBuffer中,資料庫找合適的機會再將操作Merge到資料庫中,

通過ChangeBuffer技術,我們可以把對資料庫的多次離散訪問合并為一次資料庫訪問,并且用戶的更新執行緒中不需要實際訪問磁盤,大大提升了資料庫性能,
WAL技術
不過不知道大家有沒有注意到,ChangeBuffer有一個很大的問題:如果InnoDB實體在運行期間掉電,ChangeBuffer中的快取會丟失,從而造成資料庫資料的不一致,影響資料庫事務的原子性和一致性,
資料庫中保證事務原子性和一致性通用的方案是采用WAL(Write-ahead logging,預寫式日志)技術,在使用WAL的系統中,所有的修改都先被寫入到日志中,然后再被應用到系統狀態中,日志通常包含redo和undo兩部分資訊,
- RedoLog稱為重做日志,每當有操作時,在資料變更之前將操作寫入RedoLog,這樣當發生掉電之類的情況時系統可以在重啟后繼續操作;
- UndoLog稱為撤銷日志,當一些變更執行到一半無法完成時,可以根據撤銷日志恢復到變更之間的狀態;
MySQL的InnoDB引擎中就使用了WAL技術,所以InnoDB存盤引擎包含了RedoLog和UndoLog兩部分日志,
如何確保已經提交的事務不會丟失?解決這個問題比較簡單,InnoDB有一個Log-Force-at-Commit機制,在事務提交的時候,和這個事務相關的RedoLog資料,包括Commit記錄,都必須從LogBuffer中寫入RedoLog檔案,此時事務提交成功的信號才能發送給用戶行程,通過這個機制,可以確保哪怕這個已經提交的事務中的部分ChangeBuffer還沒有被寫入資料檔案,就發生了實體故障,在做實體恢復的時候,也可以通過RedoLog的資訊,將不一致的資料前滾,
RedoLog和BinLog比較
RedoLog和BinLog不同,雖然BinLog中也記錄了InnoDB表的很多操作,也能實作重做的功能,但是它們之間有很大區別,
- BinLog是在存盤引擎的上層產生的,不管是什么存盤引擎,對資料庫進行了修改都會產生二進制日志,而RedoLog是Innodb引擎層產生的,只記錄該存盤引擎中表的修改;
- BinLog記錄資料變更的邏輯性的陳述句,如某一行資料的的變更情況或此次變更的SQL陳述句,而RedoLog是在物理格式上的日志,它記錄的是資料庫中每個頁的修改;
- BinLog只在每次事務提交的時候一次性寫入快取中的日志"檔案"(對于非事務表的操作,則是每次執行陳述句成功后就直接寫入),而RedoLog在資料準備修改前寫入快取中的RedoLog中,然后才對快取中的資料執行修改操作;而且保證在發出事務提交指令時,先向快取中的RedoLog寫入磁盤日志,寫入完成后才執行提交動作;
- BinLog只在提交的時候一次性寫入,所以BinLog記錄方式和提交順序有關,且一次提交對應一次記錄,而RedoLog中是記錄的物理頁的修改,RedoLog檔案中同一個事務可能多次記錄,最后一個提交的事務記錄會覆寫所有未提交的事務記錄,例如事務T1,可能在RedoLog中記錄了T1-1,T1-2,T1-3,T1共4個操作,其中T1表示最后提交時的日志記錄,所以對應的資料頁最終狀態是T1對應的操作結果,而且RedoLog是并發寫入的,不同事務之間的不同版本的記錄會穿插寫入到RedoLog檔案中,例如可能RedoLog的記錄方式如下: T1-1,T1-2,T2-1,T2-2,T2,T1-3,T1* ,
事務日志記錄的是物理頁的情況,它具有冪等性,因此記錄日志的方式極其簡練,冪等性的意思是多次操作前后狀態是一樣的,例如新插入一行后又洗掉該行,前后狀態沒有變化,而二進制日志記錄的是所有影響資料的操作,記錄的內容較多,例如插入一行記錄一次,洗掉該行又記錄一次,
RedoLog
RedoLog包括兩部分:一是記憶體中的日志緩沖(RedoLog Buffer),該部分日志是易失性的;二是磁盤上的重做日志檔案(RedoLog File),該部分日志是持久的,
在概念上,Innodb通過force-log-at-commit機制實作事務的持久性,即在事務提交的時候,必須先將該事務的所有事務日志寫入到磁盤上的RedoLog File和UndoLog File中進行持久化,
為了確保每次日志都能寫入到事務日志檔案中,在每次將RedoLog Buffer中的日志寫入日志檔案的程序中都會呼叫一次作業系統的fsync操作(即fsync()系統呼叫),因為MariaDB/MySQL是作業在用戶空間的,MariaDB/MySQL的RedoLog Buffer處于用戶空間的記憶體中,要寫入到磁盤上的RedoLog Buffer中,中間還要經過作業系統內核空間的作業系統快取區,呼叫fsync()的作用就是將作業系統快取區中的日志刷到磁盤上的RedoLog檔案中,
RedoLog事務日志檔案名為ib_logfileN,如:ib_logfile0,ib_logfile1......
RedoLog把日志從快取寫入磁盤的程序如下圖所示:

MySQL支持用戶自定義在事務提交時如何將日志快取中的日志刷磁盤檔案中,可以控制通過變數innodb_flush_log_at_trx_commit的值來決定,該變數有3種值:0、1、2,默認為1,但注意,這個變數只是控制事務提交時是否重繪日志快取到磁盤,
- 當設定為1的時候,事務提交時會將日志快取中的日志寫入作業系統快取,并呼叫fsync()持久化到磁盤檔案中,這種方式即使系統崩潰也不會丟失任何資料,但是因為每次提交都寫入磁盤,IO的性能較差;
- 當設定為0的時候,事務提交時不會將日志快取中的日志寫入作業系統快取,而是每秒寫入作業系統快取并呼叫fsync()持久化到磁盤檔案中,也就是說設定為0時是(大約)每秒重繪寫入到磁盤中的,當系統崩潰,會丟失1秒鐘的資料;
- 當設定為2的時候,事務提交時僅寫入到作業系統快取,然后是每秒呼叫fsync()將作業系統快取中的日志持久化到磁盤檔案中;

有一個變數
innodb_flush_log_at_timeout的值為1秒,該變數表示的是刷日志的頻率,很多人誤以為是控制innodb_flush_log_at_trx_commit值為0和2時的1秒頻率,實際上并非如此,測驗時將頻率設定為5和設定為1,當innodb_flush_log_at_trx_commit設定為0和2的時候性能基本都是不變的,關于這個頻率是控制什么的,在后面的"刷日志到磁盤的規則"中會說,
一致性的保證
在主從復制結構中,要保證事務的持久性和一致性,需要對日志相關變數設定為如下:
- 如果啟用了BinLog,則設定
sync_binlog=1,即每提交一次事務同步寫到磁盤中, - 總是設定
innodb_flush_log_at_trx_commit=1,即每提交一次事務都寫到磁盤中,
上述兩項變數的設定保證了:每次提交事務都寫入二進制日志和事務日志,并在提交時將它們重繪到磁盤中,
選擇方式1時,由于每次事務提交都會寫磁盤,在大量小事務提交的場景下會影響資料庫的性能,
RedoLog日志塊
Innodb存盤引擎中,RedoLog以塊為單位進行存的,每個塊占512位元組,這稱為RedoLog日志塊,不管是日志快取中還是系統快取以及磁盤上的RedoLog檔案,RedoLog都是這樣以512位元組的塊存盤的,

RedoLog記錄的是資料頁的變化,當一個資料頁產生的變化需要使用超過492位元組的RedoLog來記錄,那么就會使用多個RedoLog日志塊來記錄該資料頁的變化,
關于RedoLog日志塊頭的第三部分
log_block_first_rec_group,因為有時候一個資料頁產生的日志量超出了一個日志塊,這是需要用多個日志塊來記錄該頁的相關日志,例如,某一資料頁產生了552位元組的日志量,那么需要占用兩個日志塊,第一個日志塊占用492位元組,第二個日志塊需要占用60個位元組,那么對于第二個日志塊來說,它的第一個日志的開始位置就是73位元組(60+12),如果log_block_first_rec_group的值和log_block_hdr_data_len相等,則說明該日志塊中沒有新開始的日志塊,即表示該日志塊用來延續前一個日志塊,
日志尾只有一個部分:log_block_trl_no,該值和塊頭的log_block_hdr_no相等,
記憶體中的RedoLog快取和磁盤中的RedoLog檔案由多個日志塊組成,示意圖如下所示:

RedoLog日志組
RedoLog日志組由多個大小完全相同的RedoLog檔案組成,組內RedoLog檔案的數量由變數innodb_log_files_group決定,默認值為2,即兩個RedoLog檔案組成RedoLog日志組,這個組是一個邏輯的概念,并沒有真正的檔案來表示這是一個組,但是可以通過變數 innodb_log_group_home_dir來定義組的目錄,RedoLog檔案會放在這個目錄下(默認是在datadir下),
mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 16777216 |
| innodb_log_checksums | ON |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
| innodb_log_write_ahead_size | 8192 |
+-----------------------------+----------+
7 rows in set (0.06 sec)
root@b48ce1e480fd:/var/lib/mysql# ls -l ib*
-rw-r----- 1 mysql root 407 Oct 21 09:36 ib_buffer_pool
-rw-r----- 1 mysql mysql 50331648 Oct 26 09:00 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 Oct 20 07:24 ib_logfile1
-rw-r----- 1 mysql mysql 79691776 Oct 26 09:00 ibdata1
-rw-r----- 1 mysql mysql 12582912 Oct 26 09:00 ibtmp1
可以看到在MySQL默認的資料目錄下,有兩個ib_logfile開頭的檔案,它們就是RedoLog日志組中的RedoLog檔案,而且它們的大小完全一致且等于變數innodb_log_file_size定義的值,ibdata1檔案是在沒有開啟innodb_file_per_table時的共享表空間檔案,對應于開啟 innodb_file_per_table時的.ibd檔案,
在Innodb將日志快取中的RedoLog日志塊刷到RedoLog檔案中時,會以追加寫入的方式回圈輪訓寫入,即先在第一個RedoLog檔案(即ib_logfile0)的尾部追加寫,直到滿了之后向第二個RedoLog檔案(即ib_logfile1)寫,當第二個RedoLog檔案滿了會清空一部分第一個RedoLog檔案繼續寫入,
由于是將日志快取中的日志刷到RedoLog檔案,所以在RedoLog檔案中記錄日志的方式也是RedoLog日志塊的方式,RedoLog檔案的大小對Innodb的性能影響非常大,設定的太大,恢復的時候就會時間較長,設定的太小,就會導致在寫RedoLog的時候回圈切換RedoLog檔案,
在每個組的第一個RedoLog檔案中,前2KB記錄4個特定的部分,從2KB之后才開始記錄RedoLog日志塊,除了第一個RedoLog檔案中會記錄,RedoLog日志組中的其他RedoLog檔案不會記錄這2KB,但是卻會騰出這2KB的空間,

RedoLog檔案格式
Innodb存盤引擎存盤資料的單元是頁,所以RedoLog也是基于頁的格式來記錄的,默認情況下,Innodb的頁大小是16KB(由innodb_page_size變數控制),一個頁內可以存放多個RedoLog日志塊(每個512位元組),而RedoLog日志塊中記錄的又是資料頁的變化,
其中RedoLog日志塊中492位元組的部分是RedoLog內容,該RedoLog內容的格式分為4部分:
redo_log_type:占用1個位元組,表示RedoLog的日志型別;space:表示表空間的ID,采用壓縮的方式后,占用的空間可能小于4位元組;page_no:表示頁的偏移量,同樣是壓縮過的;redo_log_body表示每個重做日志的資料部分,恢復時會呼叫相應的函式進行決議,
RedoLog記錄格式
RedoLog的本質上是記錄事務對資料庫做了哪些修改, InnoDB的設計者們針對事務對資料庫的不同修改場景定義了多種型別的RedoLog日志,但是絕大部分型別的redo日志都有下邊這種通用的結構:

各個部分的詳細釋義如下:
- type:該條redo日志的型別,在MySQL 5.7.21這個版本中,InnoDB中的redo日志包含53種不同的型別,稍后會詳細介紹不同型別的redo日志,
- space ID:表空間ID,
- page number:頁號,
- data:該條redo日志的具體內容,
關于RedoLog更詳細的格式本文就不詳細做介紹,有興趣的可以自己查找檔案了解一下,我們到此處應該知道,如果我們使用Insert陳述句向資料庫中插入一條記錄,那么RedoLog會記錄要在指定空間的指定資料頁的指定地址處設定指定的值,
RedoLog刷盤策略
變數innodb_flush_log_at_trx_commit的值為1時,、事務每次提交的時候都會刷RedoLog事務日志到磁盤中,但是Innodb不僅僅只會在有ICommit動作后才會刷日志到磁盤,這只是innodb存盤引擎刷日志的規則之一,觸發日志刷盤的場景有以下幾種:
- 發出Commit動作時,Commit發出后是否刷日志由變數
innodb_flush_log_at_trx_commit控制, - 每秒刷一次,這個刷日志的頻率由變數
innodb_flush_log_at_timeout值決定,默認是1秒,要注意,這個刷日志頻率和commit動作無關, - 當log buffer中已經使用的記憶體超過一半時,
- 當有checkpoint時,checkpoint在一定程度上代表了刷到磁盤時日志所處的LSN位置,
刷臟和CheckPoint
記憶體中(BufferPool)未刷到磁盤的資料稱為臟資料(DirtyData),由于資料和日志都以頁的形式存在,所以臟頁表示臟資料和臟日志,上一節介紹了日志是何時刷到磁盤的,不僅僅是日志需要刷盤,臟資料頁也一樣需要刷盤,
在Innodb中,資料刷盤的規則只有一個:Checkpoint,但是觸發Checkpoint的情況卻有幾種,不管怎樣,Checkpoint觸發后,會將快取中臟資料頁和臟日志頁都刷到磁盤,
innodb存盤引擎中Checkpoint分為兩種:
- Sharp Checkpoint:在重用RedoLog檔案(例如切換日志檔案)的時候,將所有已記錄到RedoLog中對應的臟資料刷到磁盤,
- Fuzzy Checkpoint:一次只刷一小部分的日志到磁盤,而非將所有臟日志刷盤,有以下幾種情況會觸發該檢查點:
- Master Thread Checkpoint:由Master執行緒控制,每秒或每10秒刷入一定比例的臟頁到磁盤;
flush_lru_list checkpoint:從MySQL5.6開始可通過innodb_page_cleaners變數指定專門負責臟頁刷盤的PageCleaner執行緒的個數,該執行緒的目的是為了保證lru串列有可用的空閑頁;- Async/Sync Flush Checkpoint:同步刷盤還是異步刷盤,例如還有非常多的臟頁沒刷到磁盤(非常多是多少,有比例控制),這時候會選擇同步刷到磁盤,但這很少出現;如果臟頁不是很多,可以選擇異步刷到磁盤,如果臟頁很少,可以暫時不刷臟頁到磁盤;
- Dirty Page Too Much Checkpoint:臟頁太多時強制觸發檢查點,目的是為了保證快取有足夠的空閑空間,Too Much的比例由變數
innodb_max_dirty_pages_pct控制,MySQL 5.6默認的值為75,即當臟頁占緩沖池的百分之75后,就強制刷一部分臟頁到磁盤,由于刷臟頁需要一定的時間來完成,所以記錄檢查點的位置是在每次刷盤結束之后才在RedoLog中標記的,
MySQL停止時是否將臟資料和臟日志刷入磁盤,由變數innodb_fast_shutdown={ 0|1|2 }控制,默認值為1,即停止時只做一部分purge,忽略大多數flush操作(但至少會刷日志),在下次啟動的時候再flush剩余的內容,實作FastShutdown,
LSN學習
LSN稱為日志的邏輯序列號(Log Sequence Number),在Innodb存盤引擎中,LSN占用8個位元組,LSN的值會隨著日志的寫入而遞增,分析LSN可以得到很多關鍵資訊:
- 資料頁的版本資訊,
- 寫入的日志總量,通過LSN開始號碼和結束號碼可以計算出寫入的日志量,
- CheckPoint的位置,
LSN不僅存在于RedoLog中,還存在于資料頁中,在每個資料頁的頭部,有一個fil_page_lsn記錄了當前頁最終的LSN值是多少,通過資料頁中的LSN值和RedoLog中的LSN值比較,如果頁中的LSN值小于RedoLog中LSN值,則表示資料丟失了一部分,這時候可以通過RedoLog的記錄來恢復到RedoLog中記錄的LSN值時的狀態,
RedoLog的LSN資訊可以通過show engine innodb status來查看,MySQL 5.5版本的show結果中只有3條記錄,沒有pages flushed up to,
mysql> show engine innodb status
......
---
LOG
---
Log sequence number 12734454
Log flushed up to 12734454
Pages flushed up to 12734454
Last checkpoint at 12734445
0 pending log flushes, 0 pending chkp writes
45 log i/o's done, 0.00 log i/o's/second
其中
- log sequence number就是當前的RedoLog中的LSN,通常和快取中的LSN一致,稱為快取日志LSN;
- log flushed up to是磁盤上RedoLog檔案中的LSN,通常會比日志快取LSN小,稱為磁盤日志LSN;
- pages flushed up to是已經刷到磁盤資料頁上的LSN,稱為磁盤資料頁LSN;
- last checkpoint at是上一次檢查點所在位置的LSN,稱為CheckPoint LSN,
Innodb執行修改資料庫陳述句的流程如下所示:
- 向RedoLog快取中寫入RedoLog,并在RedoLog中記錄對應的LSN,記為快取日志LSN;
- 如果目標資料頁在快取中,修改快取中的資料頁,并在資料頁中記錄LSN,記為快取資料頁LSN;
- 日志刷回磁盤時,在RedoLog檔案中記錄對應的LSN,記為磁盤日志LSN;
- CheckPoint刷臟時快取資料頁中的LSN,記為CheckPoint LSN;
- Checkpoint要刷入的資料頁多時,刷入所有的資料頁需要一定的時間來完成,中途刷入的每個資料頁都會記下當前頁所在的LSN,暫且稱之為磁盤資料頁LSN,
如下圖展示了一個事務程序中各個LSN的變化情況:
- 12:00:00.000時刻,事務開始,初始時假設各個LSN均為
001; - 12:00:00.200時刻,執行更新陳述句1,更新快取日志LSN和快取資料頁LSN,分別加1,變更為001;
- 12:00:00.400時刻,執行更新陳述句2,更新快取日志LSN和快取資料頁LSN,分別加1,變更為002;
- 12:00:00.600時刻,執行更新陳述句3,更新快取日志LSN和快取資料頁LSN,分別加1,變更為003;
- 12:00:01.000時刻,Checkpoint,將快取中的日志和資料頁刷回磁盤,磁盤資料頁和磁盤日志的LSN更新為003,Checkpoint LSN更新為003;
- 12:00:01.200時刻,執行更新陳述句3,更新快取日志LSN和快取資料頁LSN,分別加1,變更為004;
- 12:00:01.400時刻,事務提交,快取日志寫入磁盤,磁盤日志LSN更新為004;
- 12:00:02.000時刻,Checkpoint,將快取中的日志和資料頁刷回磁盤,磁盤資料頁LSN更新為004,Checkpoint LSN更新為004;

Innodb Crash-Safe
在啟動innodb的時候,不管上次是正常關倍訓是例外關閉,總是會進行恢復操作,因為RedoLog記錄的是資料頁的物理變化,因此恢復的時候速度比邏輯日志(如BinLog)要快很多,而且,Innodb自身也做了一定程度的優化,讓恢復速度變得更快,
重啟Innodb時,Checkpoint表示已經完整刷到磁盤上資料頁的LSN,因此恢復時僅需要恢復從Checkpoint開始的日志部分,例如,當資料庫在上一次Checkpoint的LSN為10000時宕機,且事務是已經提交過的狀態,啟動資料庫時會檢查磁盤中資料頁的LSN,如果資料頁的LSN小于日志中的LSN,則會從Checkpoint開始恢復,
還有一種情況,在宕機前正處于Checkpoint的刷盤程序,且資料頁的刷盤進度超過了日志頁的刷盤進度,這時候一宕機,資料頁中記錄的LSN就會大于日志頁中的LSN,在重啟的恢復程序中會檢查到這一情況,這時超出日志進度的部分將不會重做,因為這本身就表示已經做過的事情,無需再重做,
另外,事務日志具有冪等性,所以多次操作得到同一結果的行為在日志中只記錄一次,而二進制日志不具有冪等性,多次操作會全部記錄下來,在恢復的時候會多次執行二進制日志中的記錄,速度就慢得多,例如,某記錄中id初始值為2,通過update將值設定為了3,后來又設定成了2,在事務日志中記錄的將是無變化的頁,根本無需恢復;而二進制會記錄下兩次update操作,恢復時也將執行這兩次update操作,速度比事務日志恢復更慢,
RedoLog相關變數
innodb_flush_log_at_trx_commit={0|1|2}:指定何時將事務日志刷到磁盤,默認為1;- 0表示每秒將"log buffer"同步到"os buffer"且從"os buffer"刷到磁盤日志檔案中;
- 1表示每事務提交都將"log buffer"同步到"os buffer"且從"os buffer"刷到磁盤日志檔案中;
- 2表示每事務提交都將"log buffer"同步到"os buffer"但每秒才從"os buffer"刷到磁盤日志檔案中;
innodb_log_buffer_size:log buffer的大小,默認8Minnodb_log_file_size:事務日志的大小,默認5Minnodb_log_files_group =2:事務日志組中的事務日志檔案個數,默認2個innodb_log_group_home_dir =./:事務日志組路徑,當前目錄表示資料目錄innodb_mirrored_log_groups =1:指定事務日志組的鏡像組個數,但鏡像功能好像是強制關閉的,所以只有一個RedoLog日志組,在MySQL5.7中該變數已經移除,
UndoLog
基本概念
UndoLog有兩個作用:提供回滾和多個行版本控制(MVCC),
WAL技術在資料修改的時,不僅記錄了RedoLog,還記錄了相對應的UndoLog,如果因為某些原因導致事務失敗或回滾了,可以借助該UndoLog進行回滾,
UndoLog和RedoLog記錄物理日志不一樣,它是邏輯日志,可以認為當Delete一條記錄時,UndoLog中會記錄一條對應的Insert記錄,反之亦然;當update一條記錄時,它記錄一條對應相反的update記錄,
當執行Rollback時,就可以從UndoLog中的邏輯記錄讀取到相應的內容并進行回滾,有時候應用到行版本控制的時候,也是通過UndoLog來實作的:當讀取的某一行被其他事務鎖定時,它可以從UndoLog中分析出該行記錄以前的資料是什么,從而提供該行版本資訊,讓用戶實作非鎖定一致性讀取,
UndoLog是采用段(segment)的方式來記錄的,每個undo操作在記錄的時候占用一個UndoLog Segment,
另外,UndoLog也會產生RedoLog,因為UndoLog也要實作持久性保護,
UndoLog存盤方式
Innodb存盤引擎對Undo的管理采用段的方式,Rollback Segment稱為回滾段,每個回滾段中有1024個UndoLog Segment,
在以前老版本,只支持1個Rollback Segment,這樣就只能記錄1024個UndoLog Segment,后來MySQL5.5可以支持128個Rollback Segment,即支持128*1024個Undo操作,還可以通過變數innodb_undo_logs(5.6版本以前該變數是 innodb_rollback_segments)自定義多少個Rollback Segment,默認值為128,UndoLog默認存放在共享表空間中,
root@b48ce1e480fd:/var/lib/mysql# ls -l ib*
-rw-r----- 1 mysql root 407 Oct 21 09:36 ib_buffer_pool
-rw-r----- 1 mysql mysql 50331648 Oct 26 09:00 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 Oct 20 07:24 ib_logfile1
-rw-r----- 1 mysql mysql 79691776 Oct 26 09:00 ibdata1
-rw-r----- 1 mysql mysql 12582912 Oct 26 09:00 ibtmp1
如果開啟了innodb_file_per_table,UndoLog將存盤在每個表的.ibd檔案中,在MySQL5.6中,undo的存放位置還可以通過變數innodb_undo_directory來自定義存放目錄,默認值為"."表示datadir,
默認Rollback Segment全部寫在一個檔案中,但可以通過設定變數innodb_undo_tablespaces平均分配到多少個檔案中,該變數默認值為0,即全部寫入一個表空間檔案,該變數為靜態變數,只能在資料庫示例停止狀態下修改,如寫入組態檔或啟動時帶上對應引數,
更新陳述句與UndoLog
當事務提交的時候,Innodb不會立即洗掉UndoLog,因為后續還可能會用到UndoLog,如隔離級別為Repeatable-Read時,事務讀取的都是開啟事務時的最新提交行版本,只要該事務不結束,該行版本就不能洗掉,即UndoLog不能洗掉,
但是在事務提交的時候,會將該事務對應的UndoLog放入到洗掉串列中,未來通過Purge來洗掉,并且提交事務時,還會判斷UndoLog分配的頁是否可以重用,如果可以重用,則會分配給后面來的事務,避免為每個獨立的事務分配獨立的UndoLog頁而浪費存盤空間和性能,
通過UndoLog記錄Delete和Update操作的結果發現:(insert操作無需分析,就是插入行而已)
- Delete操作實際上不會直接洗掉,而是將Delete物件打上Delete flag,標記為洗掉,最終的洗掉操作是Purge執行緒完成的,
- Update分為兩種情況:update的列是否是主鍵列,如果不是主鍵列,在UndoLog中直接反向記錄是如何Update的,即update是直接進行的;如果是主鍵列,update分兩部執行:先洗掉該行,再插入一行目標行,
UndoLog中包含了舊版本資料行的快照資訊,存盤在表空間,
BinLog和事務日志
如下圖所示,事務提交時,涉及到寫日志的地方有三個步驟:
- 寫入RedoLog,處于Prepare狀態
- 寫binlog
- 修改redo log狀態為commit

這里我們注意到在 redo log 的提交程序中引入了兩階段提交,為什么必須有 “兩階段提交” 呢?這是為了讓兩份日志之間的邏輯一致,
由于RedoLog和BinLog是兩個獨立的邏輯,如果不用兩階段提交,要么就是先寫完RedoLog再寫BinLog,或者采用反過來的順序,我們看看這兩種方式會有什么問題,用上面的更新示例做假設:
- 先寫RedoLog后寫BinLog,假設在RedoLog寫完,BinLog還沒有寫完的時候,MySQL行程例外重啟,因為RedoLog已經寫完,系統即使崩潰仍然能夠把資料恢復回來,但是BinLog里面就沒有記錄這個陳述句,因此備份日志的時候BinLog里面就沒有這條陳述句;如果需要用這個BinLog來恢復臨時庫的話,由于這個陳述句的BinLog丟失,恢復出來的值就與原庫值不同,
- 先寫BinLog后寫RedoLog,如果在BinLog寫完之后宕機,由于RedoLog還沒寫,崩潰恢復以后這個事務無效,所以這一行的值還是未更新以前的值,但是BinLog里面已經記錄了崩潰前的更新記錄,BinLog來恢復的時候就多了一個事務出來與原庫的值不同,
可以看到,兩階段提交就是為了防止BinLog和RedoLog不一致發生,同時我們也注意到為了這個崩潰恢復的一致性問題引入了很多新的東西,也讓系統復雜了很多,所以有得有失,二階段提交RedoLog和BinLog的程序中,兩者刷盤之后都會記錄2PC事務的XID(RedoLog和BinLog中事務落盤的標識),若中途資料庫Crash,通過XID關聯兩者并在恢復時決定commit和rollback與否,詳細步驟見下一段“恢復步驟”,
恢復步驟
RedoLog中的事務如果經歷了二階段提交中的Prepare階段,則會打上Prepare標識,如果經歷Commit階段,則會打上Commit標識(此時RedoLog和BinLog均已落盤):
- 按順序掃描RedoLog,如果RedoLog中的事務既有Prepare標識,又有Commit標識,就直接提交(復制RedoLog Disk中的資料頁到磁盤資料頁);
- 如果RedoLog事務只有Prepare標識,沒有Commit標識,則說明當前事務在Commit階段Crash了,RedoLog中當前事務是否完整未可知,此時拿著RedoLog中當前事務的XID(RedoLog和BinLog中事務落盤的標識),去查看binlog中是否存在此XID:
- 如果BinLog中有當前事務的XID,則提交事務(復制RedoLog disk中的資料頁到磁盤資料頁);
- 如果BinLog中沒有當前事務的XID,則回滾事務(使用UndoLog來洗掉redolog中的對應事務);
可以將MySQL中的RedoLog和BinLog二階段提交和廣義上的二階段提交進行對比,廣義上的二階段提交,若某個參與者超時未收到協調者的ack通知,則會進行回滾,回滾邏輯需要開發者在各個參與者中進行記錄,MySql二階段提交是通過xid進行恢復,
組提交
為了提高性能,通常會將有關聯性的多個資料修改操作放在一個事務中,這樣可以避免對每個修改操作都執行完整的持久化操作,這種方式,可以看作是人為的組提交(group commit),除了將多個操作組合在一個事務中,記錄binlog的操作也可以按組的思想進行優化:將多個事務涉及到的BinLog一次性Flush,而不是每次Flush一個Binlog,
事務在提交的時候不僅會記錄事務日志,還會記錄二進制日志,但是它們誰先記錄呢?BinLog是MySQL的上層日志,先于存盤引擎的事務日志被寫入,
在MySQL5.6以前,當事務提交(即發出Commit指令)后,MySQL接收到該信號進入Commit Prepare階段;進入Prepare階段后,立即寫記憶體中的BinLog日志,寫完記憶體中的BinLog日志后就相當于確定了Commit操作;然后開始寫記憶體中的事務日志;最后將BinLog日志和事務日志刷盤,它們如何刷盤,分別由變數sync_binlog和innodb_flush_log_at_trx_commit控制,
但因為要保證BinLog日志和事務日志的一致性,在提交后的Prepare階段會啟用一個prepare_commit_mutex鎖來保證它們的順序性和一致性,但這樣會導致開啟BinLog日志后Group Commmit失效,特別是在主從復制結構中,幾乎都會開啟BinLog日志,在MySQL5.6中進行了改進,提交事務時,在存盤引擎層的上一層結構中會將事務按序放入一個佇列,佇列中的第一個事務稱為Leader,其他事務稱為Follower,Leader控制著Follower的行為,雖然順序還是一樣先刷BinLog,再刷事務日志,但是機制完全改變了:洗掉了原來的prepare_commit_mutex行為,也能保證即使開啟了BinLog,Group Commit也是有效的,
MySQL5.6中分為3個步驟:flush階段、sync階段、commit階段:
- flush階段:向記憶體中寫入每個事務的BinLog;
- sync階段:將記憶體中的BinLog日志刷盤,若佇列中有多個事務,那么僅一次fsync操作就完成了二進制日志的刷盤操作,這在MySQL5.6中稱為BLGC(binary log group commit);
- commit階段:Leader根據順序呼叫存盤引擎層事務的提交,由于Innodb本就支持Group Commit,所以解決了因為鎖
prepare_commit_mutex而導致的Group Commit失效問題;
在flush階段寫入BinLog到記憶體中,但是不是寫完就進入sync階段的,而是要等待一定的時間,多積累幾個事務的binlog一起進入sync階段,等待時間由變數binlog_max_flush_queue_time決定,默認值為0表示不等待直接進入sync,設定該變數為一個大于0的值的好處是group中的事務多了,性能會好一些,但是這樣會導致事務的回應時間變慢,所以建議不要修改該變數的值,除非事務量非常多并且不斷的在寫入和更新,
進入到sync階段,會將Binlog從記憶體中刷入到磁盤,刷入的數量和單獨的Binlog日志刷盤一樣,由變數sync_binlog控制,
當有一組事務在進行commit階段時,其他新事務可以進行flush階段,它們本就不會相互阻塞,所以Group Commit會不斷生效,當然,group commit的性能和佇列中的事務數量有關,如果每次佇列中只有1個事務,那么group commit和單獨的commit沒什么區別,當佇列中事務越來越多時,即提交事務越多越快時,group commit的效果越明顯,
我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

參考檔案
MySQL實戰45講<br>
什么是 WAL<br>
詳細分析MySQL事務日志(redo log和undo log)<br>
說過的話就一定要辦到 —— redo 日志(上)<br>
本文最先發布至微信公眾號,著作權所有,禁止轉載!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/380820.html
標籤:Java
