InnoDB學習(四)之RedoLog和UndoLog


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>

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM