InnoDB學習(四)之RedoLog和UndoLog

御狐神發表於2021-12-14

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查詢語句的流程包含以下步驟:

  1. 聯結器:客戶端和MySQL服務端建立連線,使用者名稱密碼等資訊校驗;
  2. 查詢快取:如果SQL語句是查詢語句,則檢視查詢語句是否命中快取;
  3. 分析器:對SQL語句的詞法和語法進行分析,判斷SQL語句的型別和對應的表等資訊;
  4. 優化器:對SQL語句進行優化,選擇合適的索引;
  5. 執行器:在對應的MySQL引擎上執行SQL查詢語句,並返回查詢結果;

MySQL

更新語句的執行流程

如果我們不需要查詢使用者資訊,而是要更新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會成為資料庫的瓶頸,影響資料庫的效能。

MySQL

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

MySQL

通過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表的很多操作,也能實現重做的功能,但是它們之間有很大區別。

  1. BinLog是在儲存引擎的上層產生的,不管是什麼儲存引擎,對資料庫進行了修改都會產生二進位制日誌。而RedoLog是Innodb引擎層產生的,只記錄該儲存引擎中表的修改;
  2. BinLog記錄資料變更的邏輯性的語句,如某一行資料的的變更情況或此次變更的SQL語句。而RedoLog是在物理格式上的日誌,它記錄的是資料庫中每個頁的修改;
  3. BinLog只在每次事務提交的時候一次性寫入快取中的日誌"檔案"(對於非事務表的操作,則是每次執行語句成功後就直接寫入)。而RedoLog在資料準備修改前寫入快取中的RedoLog中,然後才對快取中的資料執行修改操作;而且保證在發出事務提交指令時,先向快取中的RedoLog寫入磁碟日誌,寫入完成後才執行提交動作;
  4. 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把日誌從快取寫入磁碟的過程如下圖所示:

Redolog Fsync

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日誌組

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儲存引擎刷日誌的規則之一。觸發日誌刷盤的場景有以下幾種:

  1. 發出Commit動作時,Commit發出後是否刷日誌由變數innodb_flush_log_at_trx_commit控制。
  2. 每秒刷一次。這個刷日誌的頻率由變數innodb_flush_log_at_timeout值決定,預設是1秒。要注意,這個刷日誌頻率和commit動作無關。
  3. 當log buffer中已經使用的記憶體超過一半時。
  4. 當有checkpoint時,checkpoint在一定程度上代表了刷到磁碟時日誌所處的LSN位置。

刷髒和CheckPoint

記憶體中(BufferPool)未刷到磁碟的資料稱為髒資料(DirtyData)。由於資料和日誌都以頁的形式存在,所以髒頁表示髒資料和髒日誌。上一節介紹了日誌是何時刷到磁碟的,不僅僅是日誌需要刷盤,髒資料頁也一樣需要刷盤。

在Innodb中,資料刷盤的規則只有一個:Checkpoint。但是觸發Checkpoint的情況卻有幾種。不管怎樣,Checkpoint觸發後,會將快取中髒資料頁和髒日誌頁都刷到磁碟。

innodb儲存引擎中Checkpoint分為兩種:

  • Sharp Checkpoint:在重用RedoLog檔案(例如切換日誌檔案)的時候,將所有已記錄到RedoLog中對應的髒資料刷到磁碟。
  • Fuzzy Checkpoint:一次只刷一小部分的日誌到磁碟,而非將所有髒日誌刷盤。有以下幾種情況會觸發該檢查點:
    1. Master Thread Checkpoint:由Master執行緒控制,每秒或每10秒刷入一定比例的髒頁到磁碟;
    2. flush_lru_list checkpoint:從MySQL5.6開始可通過innodb_page_cleaners變數指定專門負責髒頁刷盤的PageCleaner執行緒的個數,該執行緒的目的是為了保證lru列表有可用的空閒頁;
    3. Async/Sync Flush Checkpoint:同步刷盤還是非同步刷盤。例如還有非常多的髒頁沒刷到磁碟(非常多是多少,有比例控制),這時候會選擇同步刷到磁碟,但這很少出現;如果髒頁不是很多,可以選擇非同步刷到磁碟,如果髒頁很少,可以暫時不刷髒頁到磁碟;
    4. 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可以得到很多關鍵資訊:

  1. 資料頁的版本資訊。
  2. 寫入的日誌總量,通過LSN開始號碼和結束號碼可以計算出寫入的日誌量。
  3. 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執行修改資料庫語句的流程如下所示:

  1. 向RedoLog快取中寫入RedoLog,並在RedoLog中記錄對應的LSN,記為快取日誌LSN;
  2. 如果目標資料頁在快取中,修改快取中的資料頁,並在資料頁中記錄LSN,記為快取資料頁LSN;
  3. 日誌刷回磁碟時,在RedoLog檔案中記錄對應的LSN,記為磁碟日誌LSN;
  4. CheckPoint刷髒時快取資料頁中的LSN,記為CheckPoint LSN;
  5. Checkpoint要刷入的資料頁多時,刷入所有的資料頁需要一定的時間來完成,中途刷入的每個資料頁都會記下當前頁所在的LSN,暫且稱之為磁碟資料頁LSN。

如下圖展示了一個事務過程中各個LSN的變化情況:

  1. 12:00:00.000時刻,事務開始,初始時假設各個LSN均為001
  2. 12:00:00.200時刻,執行更新語句1,更新快取日誌LSN和快取資料頁LSN,分別加1,變更為001;
  3. 12:00:00.400時刻,執行更新語句2,更新快取日誌LSN和快取資料頁LSN,分別加1,變更為002;
  4. 12:00:00.600時刻,執行更新語句3,更新快取日誌LSN和快取資料頁LSN,分別加1,變更為003;
  5. 12:00:01.000時刻,Checkpoint,將快取中的日誌和資料頁刷回磁碟,磁碟資料頁和磁碟日誌的LSN更新為003,Checkpoint LSN更新為003;
  6. 12:00:01.200時刻,執行更新語句3,更新快取日誌LSN和快取資料頁LSN,分別加1,變更為004;
  7. 12:00:01.400時刻,事務提交,快取日誌寫入磁碟,磁碟日誌LSN更新為004;
  8. 12:00:02.000時刻,Checkpoint,將快取中的日誌和資料頁刷回磁碟,磁碟資料頁LSN更新為004,Checkpoint LSN更新為004;

LSN更新

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;
    1. 0表示每秒將"log buffer"同步到"os buffer"且從"os buffer"刷到磁碟日誌檔案中;
    2. 1表示每事務提交都將"log buffer"同步到"os buffer"且從"os buffer"刷到磁碟日誌檔案中;
    3. 2表示每事務提交都將"log buffer"同步到"os buffer"但每秒才從"os buffer"刷到磁碟日誌檔案中;
  • innodb_log_buffer_size:log buffer的大小,預設8M
  • innodb_log_file_size:事務日誌的大小,預設5M
  • innodb_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均已落盤):

  1. 按順序掃描RedoLog,如果RedoLog中的事務既有Prepare標識,又有Commit標識,就直接提交(複製RedoLog Disk中的資料頁到磁碟資料頁);
  2. 如果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_binloginnodb_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

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

參考文件

MySQL實戰45講<br>
什麼是 WAL<br>
詳細分析MySQL事務日誌(redo log和undo log)<br>
說過的話就一定要辦到 —— redo 日誌(上)<br>

本文最先發布至微信公眾號,版權所有,禁止轉載!

相關文章