MySQL之 事務日誌: redo和undo

目錄

1 概述

2 redo日誌

2.1 redo log和二進制日誌的區別

2.2 redo日誌的基本概念

2.3 日誌塊(log block)

2.4 redo log的格式

2.5  日誌刷盤的規則

2.6 數據頁刷盤的規則及checkpoint

2.7 innodb的恢復行爲

2.8 redo log相關變量

2 undo日誌

2.1 基本概念

2.2 undo日誌的存儲方式

2.3 undo日誌相關變量

2.4 delete/update操作的內部機制

3 binlog和事務日誌及group commit


1 概述

      innodb事務日誌包括redo log和undo log。redo log是重做日誌,提供前滾操作,undo log是回滾日誌,提供回滾操作。

  • redo log通常是物理日誌,記錄的是數據頁的物理修改,主要用來恢復提交後的物理數據頁(恢復數據頁,且只能恢復到最後一次提交的位置)。
  • undo一般是邏輯日誌,用來回滾行記錄到某個版本。

兩者的作用:

  • Redo Log 保證事務的持久性
  • Undo Log 保證事務的原子性(在 InnoDB 引擎中,還用 Undo Log 來實現 MVCC)

2 redo日誌

2.1 redo log和二進制日誌的區別

      redo日誌不是二進制日誌,雖然功能和二進制日誌類似,比如二進制日誌也能實現重做功能,主要區別如下:

(1)二進制日誌在存儲引擎的上層產生,且對數據庫的任何修改都可產生二進制日誌,而redo日誌在innodb層產生,只記錄該存儲引擎的修改,並且二進制入職優先於redo日誌

(2)二進制日誌是記錄操作的邏輯語句,如某一行記錄的每一列的數值,而redo日誌是物理格式上的日誌,記錄的是數據庫中每個頁的修改。

(3)二進制日誌是提交的時候一次性寫入,而redo日誌是數據準備修改前寫入緩存中的redo log,然後纔對緩存中數據進行修改。

(4)二進制日誌是一次提交記錄一次,而redo日誌可能同一事務有多次記錄。

2.2 redo日誌的基本概念

       redo log包括兩部分:一是內存中的日誌緩衝(redo log buffer),該部分日誌是易失性的;二是磁盤上的重做日誌文件(redo log file),該部分日誌是持久的。

      在概念上,innodb通過force log at commit機制實現事務的持久性,即在事務提交的時候,必須先將該事務的所有事務日誌寫入到磁盤上的redo log file和undo log file中進行持久化。

      爲了確保每次日誌都能寫入到事務日誌文件中,在每次將log buffer中的日誌寫入日誌文件的過程中都會調用一次操作系統的fsync操作(即fsync()系統調用),將OS buffer中的日誌刷到磁盤上的log file中。

MySQL支持用戶自定義在commit時如何將log buffer中的日誌刷log file中。這種控制通過變量 innodb_flush_log_at_trx_commit 的值來決定。該變量有3種值:0、1、2,默認爲1。但注意,這個變量只是控制commit動作是否刷新log buffer到磁盤。

  • 當設置爲1的時候,事務每次提交都會將log buffer中的日誌寫入os buffer並調用fsync()刷到log file on disk中。這種方式即使系統崩潰也不會丟失任何數據,但是因爲每次提交都寫入磁盤,IO的性能較差。
  • 當設置爲0的時候,事務提交時不會將log buffer中日誌寫入到os buffer,而是每秒寫入os buffer並調用fsync()寫入到log file on disk中。也就是說設置爲0時是(大約)每秒刷新寫入到磁盤中的,當系統崩潰,會丟失1秒鐘的數據。
  • 當設置爲2的時候,每次提交都僅寫入到os buffer,然後是每秒調用fsync()將os buffer中的日誌寫入到log file on disk。

注意,有一個變量 innodb_flush_log_at_timeout 的值爲1秒,該變量表示的是刷日誌的頻率。

在主從複製結構中,要保證事務的持久性和一致性,需要對日誌相關變量設置爲如下:

  • 如果啓用了二進制日誌,則設置sync_binlog=1,即每提交一次事務同步寫到磁盤中。
  • 總是設置innodb_flush_log_at_trx_commit=1,即每提交一次事務都寫到磁盤中。

上述兩項變量的設置保證了:每次提交事務都寫入二進制日誌和事務日誌,並在提交時將它們刷新到磁盤中。

       測試可知,innodb_flush_log_at_trx_commit 爲0或2時,性能差距很小,但2要比0更加安全。它們都是每秒從os buffer刷到磁盤,它們之間的時間差體現在log buffer刷到os buffer上。因爲將log buffer中的日誌刷新到os buffer只是內存數據的轉移,並沒有太大的開銷,所以每次提交和每秒刷入差距並不大。但如果數據量十分巨大,比如每秒數萬條數據,這可能導致大量數據的丟失。更好的插入數據的做法是將值設置爲1,然後修改存儲過程,將每次循環都提交修改爲只提交一次這樣既能保證數據的一致性,也能提升性能。

2.3 日誌塊(log block)

      innodb存儲引擎中,redo log以塊爲單位進行存儲的,每個塊佔512字節,這稱爲redo log block。所以不管是log buffer中還是os buffer中以及redo log file on disk中,都是這樣以512字節的塊存儲的。

     每個redo log block由3部分組成:日誌塊頭、日誌塊尾和日誌主體。其中日誌塊頭佔用12字節,日誌塊尾佔用8字節,所以每個redo log block的日誌主體部分只有512-12-8=492字節。

      因爲redo log記錄的是數據頁的變化,當一個數據頁產生的變化需要使用超過492字節()的redo log來記錄,那麼就會使用多個redo log block來記錄該數據頁的變化。

日誌塊頭包含4部分:

  •  log_block_hdr_no:(4字節)該日誌塊在redo log buffer中的位置ID。
  • log_block_hdr_data_len:(2字節)該log block中已記錄的log大小。寫滿該log block時爲0x200,表示512字節。
  • log_block_first_rec_group:(2字節)該log block中第一個log的開始偏移位置。
  • lock_block_checkpoint_no:(4字節)寫入檢查點信息的位置。

       關於log block塊頭的第三部分 log_block_first_rec_group ,因爲有時候一個數據頁產生的日誌量超出了一個日誌塊,這是需要用多個日誌塊來記錄該頁的相關日誌。例如,某一數據頁產生了552字節的日誌量,那麼需要佔用兩個日誌塊,第一個日誌塊佔用492字節,第二個日誌塊需要佔用60個字節,那麼對於第二個日誌塊來說,它的第一個log的開始位置就是73字節(60+12)。如果該部分的值和 log_block_hdr_data_len 相等,則說明該log block中沒有新開始的日誌塊,即表示該日誌塊用來延續前一個日誌塊。

2.4 redo log的格式

       redo log也是基於頁的格式來記錄的。默認情況下,innodb的頁大小是16KB(由 innodb_page_size 變量控制),一個頁內可以存放 非常多的log block(每個512字節),而log block中記錄的又是數據頁的變化。

其中log block中492字節的部分是log body,該log body的格式分爲4部分:

  • redo_log_type:佔用1個字節,表示redo log的日誌類型。
  • space:表示表空間的ID,採用壓縮的方式後,佔用的空間可能小於4字節。
  • page_no:表示頁的偏移量,同樣是壓縮過的。
  • redo_log_body表示每個重做日誌的數據部分,恢復時會調用相應的函數進行解析。例如insert語句和delete語句寫入redo log的內容是不一樣的。

如下圖,分別是insert和delete大致的記錄方式。

2.5  日誌刷盤的規則

log buffer中未刷到磁盤的日誌稱爲髒日誌(dirty log)。

在上面的說過,默認情況下事務每次提交的時候都會刷事務日誌到磁盤中,這是因爲變量 innodb_flush_log_at_trx_commit 的值爲1。但是innodb不僅僅只會在有commit動作後纔會刷日誌到磁盤,這只是innodb存儲引擎刷日誌的規則之一。

刷日誌到磁盤有以下幾種規則:

  • 發出commit動作時。已經說明過,commit發出後是否刷日誌由變量 innodb_flush_log_at_trx_commit 控制。
  • 每秒刷一次。這個刷日誌的頻率由變量 innodb_flush_log_at_timeout 值決定,默認是1秒。要注意,這個刷日誌頻率和commit動作無關。
  • 當log buffer中已經使用的內存超過一半時。
  • 當有checkpoint時,checkpoint在一定程度上代表了刷到磁盤時日誌所處的LSN位置。

2.6 數據頁刷盤的規則及checkpoint

      內存中(buffer pool)未刷到磁盤的數據稱爲髒數據(dirty data)。由於數據和日誌都以頁的形式存在,所以髒頁表示髒數據和髒日誌。

      在innodb中,數據刷盤的規則只有一個:checkpoint。但是觸發checkpoint的情況卻有幾種。在checkpoint觸發後,會將buffer中髒數據頁和髒日誌頁都刷到磁盤

innodb存儲引擎中checkpoint分爲兩種:

  • sharp checkpoint:在重用redo log文件(例如切換日誌文件)的時候,將所有已記錄到redo log中對應的髒數據刷到磁盤。
  • fuzzy checkpoint:一次只刷一小部分的日誌到磁盤,而非將所有髒日誌刷盤。有以下幾種情況會觸發該檢查點:
    • master thread checkpoint:由master線程控制,每秒或每10秒刷入一定比例的髒頁到磁盤
    • flush_lru_list checkpoint:從MySQL5.6開始可通過 innodb_page_cleaners 變量指定專門負責髒頁刷盤的page cleaner線程的個數,該線程的目的是爲了保證lru列表有可用的空閒頁。
    • async/sync flush checkpoint:同步刷盤還是異步刷盤。例如還有非常多的髒頁沒刷到磁盤(非常多是多少,有比例控制),這時候會選擇同步刷到磁盤,但這很少出現;如果髒頁不是很多,可以選擇異步刷到磁盤,如果髒頁很少,可以暫時不刷髒頁到磁盤
    • dirty page too much checkpoint:髒頁太多時強制觸發檢查點,目的是爲了保證緩存有足夠的空閒空間。too much的比例由變量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默認的值爲75,即當髒頁佔緩衝池的百分之75後,就強制刷一部分髒頁到磁盤。

由於刷髒頁需要一定的時間來完成,所以記錄檢查點的位置是在每次刷盤結束之後纔在redo log中標記的。

      MySQL停止時是否將髒數據和髒日誌刷入磁盤,由變量innodb_fast_shutdown={ 0|1|2 }控制,默認值爲1,即停止時只做一部分purge,忽略大多數flush操作(但至少會刷日誌),在下次啓動的時候再flush剩餘的內容,實現fast shutdown。

2.7 innodb的恢復行爲

      在啓動innodb的時候,不管上次是正常關閉還是異常關閉,總是會進行恢復操作。

      因爲redo log記錄的是數據頁的物理變化,因此恢復的時候速度比邏輯日誌(如二進制日誌)要快很多。而且,innodb自身也做了一定程度的優化,讓恢復速度變得更快。

       重啓innodb時,checkpoint表示已經完整刷到磁盤上data page上的LSN,因此恢復時僅需要恢復從checkpoint開始的日誌部分。例如,當數據庫在上一次checkpoint的LSN爲10000時宕機,且事務是已經提交過的狀態。啓動數據庫時會檢查磁盤中數據頁的LSN,如果數據頁的LSN小於日誌中的LSN,則會從檢查點開始恢復。

       還有一種情況,在宕機前正處於checkpoint的刷盤過程,且數據頁的刷盤進度超過了日誌頁的刷盤進度。這時候一宕機,數據頁中記錄的LSN就會大於日誌頁中的LSN,在重啓的恢復過程中會檢查到這一情況,這時超出日誌進度的部分將不會重做,因爲這本身就表示已經做過的事情,無需再重做。

2.8 redo log相關變量

  • 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的大小,默認8M
  • innodb_log_file_size:#事務日誌的大小,默認5M
  • innodb_log_files_group =2:# 事務日誌組中的事務日誌文件個數,默認2個
  • innodb_log_group_home_dir =./:# 事務日誌組路徑,當前目錄表示數據目錄
  • innodb_mirrored_log_groups =1:# 指定事務日誌組的鏡像組個數,但鏡像功能好像是強制關閉的,所以只有一個log group。在MySQL5.7中該變量已經移除。

2 undo日誌

2.1 基本概念

      undo log有兩個作用:提供回滾和多個行版本控制(MVCC)。

      在數據修改的時候,不僅記錄了redo,還記錄了相對應的undo,如果因爲某些原因導致事務失敗或回滾了,可以藉助該undo進行回滾。

       undo log和redo log記錄物理日誌不一樣,它是邏輯日誌。可以認爲當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄

      當執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾。有時候應用到行版本控制的時候,也是通過undo log來實現的:當讀取的某一行被其他事務鎖定時,它可以從undo log中分析出該行記錄以前的數據是什麼,從而提供該行版本信息,讓用戶實現非鎖定一致性讀取。

      undo log是採用段(segment)的方式來記錄的,每個undo操作在記錄的時候佔用一個undo log segment

      另外,undo log也會產生redo log,因爲undo log也要實現持久性保護

2.2 undo日誌的存儲方式

      innodb存儲引擎對undo的管理採用段的方式。rollback segment稱爲回滾段,每個回滾段中有1024個undo log segment

      在以前老版本,只支持1個rollback segment,這樣就只能記錄1024個undo log segment。後來MySQL5.5可以支持128個rollback segment,即支持128*1024個undo操作,還可以通過變量 innodb_undo_logs自定義多少個rollback segment,默認值爲128。

2.3 undo日誌相關變量

主要有如下變量:

 mysql> show variables like "%undo%";
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_undo_directory   | .     |
| innodb_undo_logs        | 128   |
| innodb_undo_tablespaces | 0     |
+-------------------------+-------+

2.4 delete/update操作的內部機制

      當事務提交的時候,innodb不會立即刪除undo log,因爲後續還可能會用到undo log,如隔離級別爲repeatable read時,事務讀取的都是開啓事務時的最新提交行版本,只要該事務不結束,該行版本就不能刪除,即undo log不能刪除。

      但是在事務提交的時候,會將該事務對應的undo log放入到刪除列表中,未來通過purge來刪除。並且提交事務時,還會判斷undo log分配的頁是否可以重用,如果可以重用,則會分配給後面來的事務,避免爲每個獨立的事務分配獨立的undo log頁而浪費存儲空間和性能。

通過undo log記錄delete和update操作的結果發現:(insert操作無需分析,就是插入行而已)

  • delete操作實際上不會直接刪除,而是將delete對象打上delete flag,標記爲刪除,最終的刪除操作是purge線程完成的。
  • update分爲兩種情況:update的列是否是主鍵列。
    • 如果不是主鍵列,在undo log中直接反向記錄是如何update的。即update是直接進行的。
    • 如果是主鍵列,update分兩部執行:先刪除該行,再插入一行目標行。

3 binlog和事務日誌及group commit

      如果事務不是隻讀事務,即涉及到了數據的修改,默認情況下會在commit的時候調用fsync()將日誌刷到磁盤,保證事務的持久性。且在事務量非常大的時候。innodb提供了group commit功能,可以將多個事務的事務日誌通過一次fsync()刷到磁盤中。

      在MySQL5.6中提交事務時,在存儲引擎層的上一層結構中會將事務按序放入一個隊列,隊列中的第一個事務稱爲leader,其他事務稱爲follower,leader控制着follower的行爲。雖然順序還是一樣先刷二進制,再刷事務日誌,但是機制完全改變了。 

MySQL5.6中分爲3個步驟:flush階段、sync階段、commit階段

  • flush階段:向內存中寫入每個事務的二進制日誌。
  • sync階段:將內存中的二進制日誌刷盤。若隊列中有多個事務,那麼僅一次fsync操作就完成了二進制日誌的刷盤操作。這在MySQL5.6中稱爲BLGC(binary log group commit)。
  • commit階段:leader根據順序調用存儲引擎層事務的提交,由於innodb本就支持group commit,所以解決了因爲鎖 prepare_commit_mutex 而導致的group commit失效問題。

       在flush階段寫入二進制日誌到內存中,但是不是寫完就進入sync階段的,而是要等待一定的時間,多積累幾個事務的binlog一起進入sync階段,等待時間由變量 binlog_max_flush_queue_time 決定,默認值爲0表示不等待直接進入sync,設置該變量爲一個大於0的值的好處是group中的事務多了,性能會好一些,但是這樣會導致事務的響應時間變慢,所以建議不要修改該變量的值,除非事務量非常多並且不斷的在寫入和更新。

      進入到sync階段,會將binlog從內存中刷入到磁盤,刷入的數量和單獨的二進制日誌刷盤一樣,由變量 sync_binlog 控制。

      當有一組事務在進行commit階段時,其他新事務可以進行flush階段,它們本就不會相互阻塞,所以group commit會不斷生效。當然,group commit的性能和隊列中的事務數量有關,如果每次隊列中只有1個事務,那麼group commit和單獨的commit沒什麼區別,當隊列中事務越來越多時,即提交事務越多越快時,group commit的效果越明顯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章