MySQL 中的WAL機制

本文主要概括的是 WAL 機制涉及的三種日誌,具體與其他組件的聯繫、執行可查看 一條 sql 的執行過程詳解 、MySQL中的事務原理和鎖機制 。

是什麼

WAL,全稱是Write-Ahead Logging, 預寫日誌系統。指的是 MySQL 的寫操作並不是立刻更新到磁碟上,而是先記錄在日誌上,然後在合適的時間再更新到磁碟上。這樣的好處是錯開高峰期。日誌主要分為 undo log、redo log、binlog。這三種在之前的部落格已經詳細說過了,作用分別是 ” 完成MVCC從而實現 MySQL 的隔離級別 “、” 降低隨機寫的性能消耗(轉成順序寫),同時防止寫操作因為宕機而丟失 “、” 寫操作的備份,保證主從一致 “。關於這三種日誌的內容講的比較分散且具體的執行過程沒有提到,所以這裡來總結一下這三種日誌。

 

undo log

undo log 主要用於實現 MVCC,從而實現 MySQL 的 」讀已提交「、」可重複讀「 隔離級別。在每個行記錄後面有兩個隱藏列,”trx_id”、”roll_pointer”,分別表示上一次修改的事務id,以及 “上一次修改之前保存在 undo log中的記錄位置 “。在對一行記錄進行修改或刪除操作前,會先將該記錄拷貝一份到 undo log 中,然後再進行修改,並將修改事務 id,拷貝的記錄在 undo log 中的位置寫入 “trx_id”、”roll_pointer”。

而 MVCC 最核心的就是 版本鏈 和通過版本鏈生成的 Read View

1、版本鏈:通過 “roll_pointer” 欄位指向的上一次修改的值,使每行記錄變化前後形成了一條版本鏈。

2、Read View:Read View 表示可見視圖,用於限制當前事務查詢數據的,通過與版本鏈的配合可以實現對數據的 「快照讀」 。Read View 內部主要有四個部分組成,第一個是創建當前 Read View 的事務 id creator_trx_id,第二個是創建 Read View 時還未提交的事務 id 集合trx_ids,第三個是未提交事務 id 集合中的最大值up_limit_id,第四個是未提交事務 id 集合中的最小值low_limit_id。

當執行查詢操作時會先找磁碟上的數據,然後根據 Read View 里的各個值進行判斷,

1)如果該數據的 trx_id 等於 creator_trx_id,那麼就說明這條數據是創建 Read View的事務修改的,那麼就直接返回;

2)如果 trx_id 大於等於 up_limit_id,說明是新事務修改的,那麼會根據 roll_pointer 找到上一個版本的數據重新比較;

3)如果 trx_id 小於 low_limit_id,那麼說明是之前的事務修改的數據,那麼就直接返回;

4)如果 trx_id 是在 low_limit_id 與 up_limit_id 中間,那麼需要去 trx_ids 中對各個元素逐個判斷,如果存在相同值的元素,就根據 roll_pointer 找到上一個版本的數據,然後再重複判斷;如果不存在就說明該數據是創建當前 Read View 時就已經修改好的了,可以返回。

 

而讀已提交和可重複讀之所以不同就是它們 Read View 生成機制不同,讀已提交是每次 select 都會重新生成一次,而可重複讀是一次事務只會創建一次且在第一次查詢時創建 Read View。事務啟動命令begin/start transaction不會創建Read View,但是通過 start transaction with consistent snapshot 開啟事務就會在開始時就創建一次 Read View。

 

舉個網上的例子,啟動事務的方式是通過 start transaction with consistent 。首先創建事務1,假設此時事務1 id 是60,事務1先修改 name 為小明1,那麼就會在修改前將之前的記錄寫入 undo log,同時在修改時將生成的undo log 行數據地址寫入 roll_pointer,然後暫不提交事務1。開一個事務2,事務 id 為 65,進行查詢操作,此時生成的 Read View 的trx_ids是[60],creator_trx_id 為 65,對應的數據狀態就是下圖,首先先得到磁碟數據的 trx_id ,為60,然後判斷,不等於 creator_trx_id,然後檢查,最大值和最小值都是 60,也就是屬於上面 2)的情況,所以通過 roll_pointer 從 undo log 中找到 「小明」 那條數據,再次判斷,發現 50 是小於 60的,滿足上面 3)的情況,所以返回數據。

然後提交事務1,再開一個事務3,將name改成小明2,假設此時的事務3 id 是100,那麼在修改前又會將 trx_id 為 60 拷貝進 undo log,同時修改時將 trx_id 改為100,然後事務3暫不提交,此時事務1再進行select。如果隔離級別是讀已提交,那麼就會重新生成 Read View,trx_ids是[100],creator_trx_id 為65,判斷過程和上面相似,最終返回的是小明1那條數據;而如果是可重複讀,那麼還是一開始的 Read View,trx_ids 還是[60],creator_trx_id 還是 65,那麼還是從小明2 的 trx_id 進行判斷,發現不等於 65,且大於60,為情況 2),跳到 小明1 ,對 trx_id判斷,還是大於,還是情況 2),跳轉到 「小明」 那條數據,判斷 trx_id <  low_mimit_id,為情況 3),所以返回 “小明”。下面是這個例子最終的示意圖

 

 

Redo Log 與 Binlog

Redo log

redo log 是搭配緩衝池、change buffer 使用的,緩衝池的作用是快取磁碟上的數據頁,減少磁碟的IO;change buffer 的作用是將寫操作先存在記憶體中,等到下次需要讀取這些操作涉及到的數據頁時,就把數據頁載入到緩衝池中,然後在緩衝池中更新;redo log 的作用就是持久化記錄的寫操作,防止在寫操作更新到磁碟前發生斷電丟失這些寫操作,直到該操作對應的臟頁真正落盤(先讀取數據頁到緩衝池然後應用寫操作到緩衝池,最後再將臟頁落盤替換磁碟上的數據頁),該操作才會從 redo log 中移除。記錄的是寫操作對數據頁的修改邏輯以及 change buffer的變化。

三種狀態

在將寫操作寫入 redo log 的過程中並不是直接就進行磁碟IO來完成的,而是分為三個步驟。

1、寫入 redo log buffer 中,這部分是屬於MySQL 的記憶體中,是全局公用的。

2、在事務編寫完成後,就可以執行 write 操作,寫到文件系統的 page cache 中,屬於作業系統的記憶體,如果 MySQL 崩潰不會影響,但如果機器斷電則會丟失數據。

3、執行 fsync(持久化)操作,將 page cache 中的數據正式寫入磁碟上的 redo log 中,也就是圖中的 hard disk。

 

redo log 的持久化

1、持久化策略通過參數 innodb_flush_log_at_trx_commit 控制。

設置為 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ; MySQL 崩潰就會丟失。
設置為 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁碟(將 redo log buffer 中的操作全部進行持久化,可能會包含其他事務還未提交的記錄);斷電也不會丟失。
設置為 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。MySQL 崩潰不會丟失,斷電會丟失。

2、InnoDB 後台還有一個執行緒會每隔一秒鐘將 redo log buffer 中記錄的操作執行 write 寫到 page cache,然後再 fsync 到磁碟上。 

 

未提交的事務操作也可能會持久化,未提交事務操作的持久化觸發場景如下:

1、redo log buffer 被佔用的空間達到 innodb_log_buffer_size(redo log buffer 大小參數)的一半時,後台會主動寫盤,無論是否是已完成的事務操作都會執行。

2、innodb_flush_log_at_trx_commit 設為 1 時,在每次事務提交時,都會將 redo log buffer 中的所有操作(包括未提交事務的操作)都進行持久化。

3、後台有執行緒每秒清空 redo log buffer 進行落盤。

 

Binlog

binlog 也是保存寫操作的,但是它主要是用於進行集群中保證主從一致以及執行異常操作後恢複數據的。

三種格式

1、Row(5.7默認)。記錄操作語句對具體行的操作以及操作前的整行資訊。缺點是占空間大。優點是能保證數據安全,不會發生遺漏

內容可以通過 ” mysqlbinlog + 文件名 ” 來查看,一個事務的結尾會有 ” Xid” 標記(作為三步提交時判斷事務是否執行完成的判斷標記),內容格式如下:

1)server id 1,表示這個事務是在 server_id=1 的這個庫上執行的。
2)每個 event 都有 CRC32 的值,這是因為我把參數 binlog_checksum 設置成了 CRC32。
3)Table_map event 跟在圖 5 中看到的相同,顯示了接下來要打開的表,map 到數字 226。現在我們這條 SQL 語句只操作了一張表,如果要操作多張表呢?每個表都有一個對應的 Table_map event、都會 map 到一個單獨的數字,用於區分對不同表的操作。
4)我們在 mysqlbinlog 的命令中,使用了 -vv 參數是為了把內容都解析出來,所以從結果裡面可以看到各個欄位的值(比如,@1=4、 @2=4 這些值)。
5)binlog_row_image 的默認配置是 FULL,因此 Delete_event 裡面,包含了刪掉的行的所有欄位的值。如果把 binlog_row_image 設置為 MINIMAL,則只會記錄必要的資訊,在這個例子里,就是只會記錄 id=4 這個資訊。
6)最後的 Xid event,用於表示事務被正確地提交了。

2、Statement。記錄修改的 sql。缺點是在 mysql 集群時可能會導致操作不一致從而使得數據不一致(比如在操作中加入了Now()函數,主從資料庫操作的時間不同結果也不同)。優點是占空間小,執行快。

可以使用 “show binlog events in ‘文件名'” 來查看 statement 格式的日誌內容(通用),一個事務的結尾會有 ” COMMIT ” 標誌。 內容格式如下:

3、Mixed。會針對於操作的 sql 選擇使用Row 還是 Statement。相比於 row 更省空間,但還是可能發生主從不一致的情況

 

三種狀態

和 redo log 類似,binlog 寫到磁碟上的過程也分為三種狀態:binlog cache(每個執行緒各有一份)、page chache、disk。

write:從binglog cache寫到 page cache。

fsync:將數據持久化到磁碟。

 

binlog 的持久化

binlog 的持久化策略通過參數 sync_binlog 控制:

sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync;
sync_binlog=1 的時候,表示每次提交事務都會執行 fsync;
sync_binlog=N(N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務後才 fsync。

 

兩者的聯繫

狀態

兩者都經歷三種狀態: MySQL 的 Cache、Page cache、磁碟。只不過 redo log 在 MySQL 的 Cache 是全局共用的,而 binlog 在 MySQL 中的 Cache 是執行緒私有的,每個執行緒都有一份。同時兩者的 write 操作寫入 Page Cache 都非常快(因為在記憶體中),而 fsync 到磁碟都比較慢(因為需要進行磁碟IO)。

 

Crash-Safe 能力

Crash-safe 能力,指的是在機器突然斷電重啟後,之前的數據不會丟失,能夠恢復成斷電前狀態的能力。redo log 擁有 crash-safe 能力,而 binlog 沒有。這是因為 redo log 記錄的是未更新到磁碟上的操作,在斷電後只需要將記錄的操作數據更新到緩衝池中就可以了。而 binlog 記錄的是所有請求過來的寫操作,這個寫操作在斷電前有沒有落盤並不知道。也正因為如此所以採用 redo log 與 binlog 的 「 三步提交 」 來保證 binlog 也具有 crash-safe 能力。

” 三步提交 ” 過程是 ” 寫 redo log(prepare)—–> 寫 binlog ——–> redo log (commit) “。在斷電重啟後先檢查 redo log 記錄的事務操作是否為 commit 狀態

1、如果是 commit 狀態說明沒有數據丟失,判斷下一個。

2、如果是 prepare 狀態,檢查 binlog 記錄的對應事務操作(redo log 與 binlog 記錄的事務操作有一個共同欄位 XID,redo log 就是通過這個欄位找到 binlog 中對應的事務的)是否完整(這點在前面 binlog 三種格式分析過,每種格式記錄的事務結尾都有特定的標識),如果完整就將 redo log 設為 commit 狀態,然後結束;不完整就回滾 redo log 的事務,結束。

 

三步提交的參數配置

上面說到 redo log 與 binlog 的 「 三步提交 」 可以使 binlog 也具有 crash-safe 能力,但是並不是絕對的,” 三步提交 ” 還需要搭配合適的 redo log 與 binlog 的持久化策略才可以完全保證斷電重啟後操作數據不會丟失

如果想要資料庫擁有 ” crash-safe ” 能力,那麼就需要將 redo log 的持久化策略參數 innodb_flush_log_at_trx_commit 設為1,binlog 的持久化策略參數 sync_binlog 設為大於0

1、首先 innodb_flush_log_at_trx_commit  如果設為 「 非1 」,那麼斷電後一定會丟失 redo log 記錄的數據,而binlog 也就失去了 「 參照物 」,造成主從不一致。

2、而如果 sync_binlog 設為 0 時,在斷電後會丟失所有數據;等於1 會丟失還未 fsync 完成的事務數據;大於1時會在斷電後丟失上一次 fsync 到現在所有未完成 fsync 的事務數據。1 和 大於1 的區別就是 大於1 會更節省 CPU 資源,但是在斷電後會丟失更多的事務操作,所以在一般情況下都使用 「 雙 1 配置 」,也就是將 sync_binlog 和 innodb_flush_log_at_trx_commit 都設為 1, 這樣搭配 「 三步提交 」 可以在最大程度上保證數據的完整性。最多也只會丟失一條事務操作,然後回滾就可以了。

但是 「 雙 1 配置 」 伴隨著巨大的性能消耗,所以在某些場景下不適合使用 「 雙 1 配置 」。

1、業務高峰期,系統執行緩慢;

2、備庫延遲較高,需要讓備庫儘快趕上主庫;

3、批量導入數據時。

上面這些非雙1場景一般設置:innodb_flush_logs_at_trx_commit=2、sync_binlog=1000。

 

組提交優化 ” 三步提交 “

通過上面的分析知道 「 雙1配置 」 可以更完整得具有 crash-safe 能力,但是這樣配置會給系統帶來更大的 IO 壓力,因為這樣配置就需要在每次事務提交時都進行一次 redo log 與 binlog 的磁碟 IO,帶來的壓力是非常大的,那麼有沒有什麼方式來緩解呢?組提交就是用來減少 redo log、binlog 帶來的磁碟 IO 壓力的。

實現方式:日誌邏輯序列號(log sequence number,LSN)表示redo log記載的寫入點,也就是最新寫入事務的開始點,其前面都是已寫完的事務。因為 redo log 寫入 「 redo log buffer 完成 「 到 ” write 到 page cache”、” 正式開始 fsync ” 需要時間,在這個時間內可能伴隨著多個事務的寫入完成,那麼就可以以第一個事務為準,在持久化時將操作記錄完成的事務合併一起進行 fsync。執行過程如下:

1、trx1 是第一個到達的,會被選為這組的 leader;
2、等 trx1 要開始寫盤的時候,這個組裡面已經有了三個事務,這時候 LSN 也變成了 160;
3、trx1 去寫盤的時候,帶的就是 LSN=160,因此等 trx1 返回時,所有 LSN 小於等於 160 的 redo log,都已經被持久化到磁碟;
4、這時候 trx2 和 trx3 就可以直接返回了。

這樣原本 trx1、trx2、trx3 需要三次磁碟 IO,而引入組提交後只需要執行一次就可以了。而在並發更新的場景下,第一個事務寫到 redo log buffer後,越晚 fsync,組內堆積的事務就越多,組提交提高的效率也就越高,所以在三步提交中 redo log 的 prepare 寫是分為兩部分,首先執行write 操作寫到 page cache 後會先執行 binlog 的 write 操作,執行結束後再執行 redo log  prepare 狀態的 fsync 操作。這樣就可以延長 fsync 的時間,提高組提交節省的資源。

因為 binlog 也擁有組提交,所以這樣執行也可以提高 binlog 的 IO 消耗,但單條 redo log 的 fsync 執行的很快,為了進一步提高 binlog 組提交節省的資源,還可以通過參數 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 來延長 binlog 執行 fsync 的時間。  

binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用 fsync;
binlog_group_commit_sync_no_delay_count 參數,表示累積多少次以後才調用 fsync。
兩個條件是或的關係,也就是說只要有一個滿足條件就會調用 fsync。
所以,當 binlog_group_commit_sync_delay 設置為 0 的時候,binlog_group_commit_sync_no_delay_count 也無效了。

 

三步提交過程總結

在 redo log 持久化參數 innodb_flush_log_at_trx_commit 設為 1 時,每次提交、每秒鐘都會清空 redo log buffer 來執行三步提交,而在兩個日誌 fsync 持久化時還會分組來進行組提交,減少磁碟IO 次數。

特點:

1、組提交是以組為單位按順序進行寫操作的,從 redo log prepare 狀態開始到 redo log commit 狀態同一時刻只會有一個組的事務在執行。

2、一個組的事務中的操作對某一行的操作一定是唯一的。因為如果兩個事務對同一行記錄進行操作,那麼一定有一個事務會被行鎖所阻塞,導致其不會跟另一個事務在同一個組內。

 

三個日誌的比較(undo、redo、bin)

1、undo log是用於事務的回滾、保證事務隔離級別讀已提交、可重複讀實現的。redo log是用於對暫不更新到磁碟上的操作進行記錄,使得其可以延遲落盤,保證程式的效率。bin log是對數據操作進行備份恢復(並不能依靠 bin log 直接完成數據恢復)。

2、undo log 與 redo log 是存儲引擎層的日誌,只能在 InnoDB 下使用;而bin log 是 Server 層的日誌,可以在任何引擎下使用。

3、redo log 大小有限,超過後會循環寫;另外兩個大小不會。

4、undo log 記錄的是行記錄變化前的數據;redo log 記錄的是 sql 的數據頁修改邏輯以及 change buffer 的變更;bin log記錄操作語句對具體行的操作以及操作前的整行資訊(5.7默認)或者sql語句。

5、單獨的 binlog 沒有 crash-safe 能力,也就是在異常斷電後,之前已經提交但未更新的事務操作到磁碟的操作會丟失,也就是主從複製的一致性無法保障,而 redo log 有 crash-safe 能力,通過與 redo log 的配合實現 “三步提交”,就可以讓主從庫的數據也能保證一致性。

6、redo log 是物理日誌,它記錄的是數據頁修改邏輯以及 change buffer 的變更,只能在當前存儲引擎下使用,而 binlog 是邏輯日誌,它記錄的是操作語句涉及的每一行修改前後的值,在任何存儲引擎下都可以使用。

 

Tags: