1. 程式人生 > 程式設計 >圖文並茂,帶你瞭解SQL更新的過程

圖文並茂,帶你瞭解SQL更新的過程

redo log 和 bin log

在DML語句執行的過程中,主要會涉及到兩個日誌——redo log和bin log,而這兩個日誌是資料庫 WAL (Write Ahead Logging,先寫日誌再寫磁碟提高效率) 技術的兩大主角。下面我來介紹一下這兩個日誌。

redo log(重做日誌)

  • 型別:資料頁級別的,記錄的是物理日誌 (比如某個資料做了是什麼更改)。
  • 作用:確保事務的永續性,防止在資料庫 crash 的時候上有髒頁未寫入磁碟,在重啟 MySQL 的時候會根據 redo log 進行重做。
  • 產生時間:在事務開始的時候就會產生,而 redo log 的落盤不是在事務提交的時候,而是在事務執行過程中就會進行 redo log 的寫入
  • 釋放時間:當記憶體中的髒頁都寫入磁碟了,那麼相應的 redo log 就會被覆蓋

注意: 這裡為什麼說是覆蓋是因為 redo log 寫日誌的特性。redo log 的大小是固定的,所以寫 redo log 是迴圈的覆蓋寫,你可以理解為,一個環形檔案如下圖。

其中,整個環形是兩個檔案構成的(檔案個數和檔案大小你可以自己指定),兩個檔案像連在一起一樣,其中綠色標識的是 check point ,用來表示當前日誌被清理到的頭(可以理解為當前有用的 redo log的頭),而 write position 代表著當前寫入位置(就是當前有用 redo log 的尾)。如果資料庫有更新那麼 write position 就會向前推,如果 write position 要追上 check point 的時候,那麼資料庫就會停下來將 check point 向前推(就是清理,此時就是將記憶體中的髒頁進行寫入磁碟,對應著上面的釋放時間)。

bin log

bin log 預設是關閉的,需要在配置檔案自己設定。

  • 型別:資料行級別的,邏輯日誌 (有兩種形式,一種是 statement ,記錄著sql語句,另一種是 row ,記錄著資料行更新前和更新後的內容)。
  • 作用:主要用於實現 MySQL 主從複製,資料備份和資料恢復。
  • 產生時間:在一個事務提交的時候會被寫入磁碟。
  • 釋放時間:是追加寫的,所以不會被覆蓋,無釋放時間。

DML 的執行流程

如果你對 MySQL 的這兩個日誌沒有了解過的話,上面的特性是很難理解的,如果結合著 DML 語句執行流程就會好理解一點,比如我現在要在資料庫的表中更新 id = 1 這一行中的 value 欄位。

update table set value = value + 1 where id = 1;
複製程式碼

這個時候更新的大致流程就是這樣的

  1. 首先 MySQL 的 server 層會通過呼叫執行器去獲取指定資料行
  2. 苦差事當然交給引擎(這裡是innodb)來做,InnoDB 首先會去檢視當前記憶體中是否存在該資料行,如果存在之間從記憶體中取出,如果不在那麼會從磁碟中 load 到記憶體之後再從記憶體中取出相應資料行。
  3. 然後將資料行進行更新並將新行寫入記憶體中(注意此時肯定會產生髒頁,後面會瞭解到)。
  4. 之後就會開始寫日誌,首先是 redo log的寫入(此時進入prepare狀態*)。
  5. 第二個寫 bin log。
  6. 最後進行事務的提交。

注意:這裡的事務提交不僅僅是簡單的 commit; ,因為這裡只是簡單的 update 語句,自己本身就是一個事務,所以這裡的 commit; 是隱式的。而這裡所說的 commit; 還包含了 redo log 的狀態轉換——從 prepare 到 commit 狀態,這是一個很重要的點,後面我會詳細解釋,你這裡需要記住有這麼一個東西。

到這裡我們來簡單總結一下:

DML語句的執行和兩個日誌——redo log、bin log有著很大的關係,因為需要提高資料庫的效能,MySQL 採用了一種 WAL(先寫日誌再寫磁碟) 技術,其中就使用到了這兩個日誌。主要的流程如下,MySQL會從記憶體中獲取相應的資料行(如果沒有先從磁碟 load 到記憶體中),然後將資料行進行更新並將新行寫入記憶體後進行redo log的寫入和 bin log 的寫入,在一開始 redo log 是處於 prepare 狀態,只有在 bin log 寫完然後進行事務提交的時候才會處於 commit 狀態

不僅僅是那麼簡單

這個時候你肯定有幾個疑問。

redo log是如何保證事務的永續性的?(即當事務執行期間發生 crash ,redo log是如何保證 crash-safe 能力)

bin log是如何完成資料恢復和主從複製的?

上面redo log的 prepare 和 commit 兩個狀態的存在意義是什麼?

為什麼要存在兩個日誌,只要一個不行嗎?

為什麼 WAL 技術能提高資料庫效能?

下面我來慢慢回答這些問題。

bin log是如何完成資料恢復和主從複製的

首先最簡單的是第二個,bin log是如何完成資料恢復和主從複製的?,看了上面的介紹大家應該也知道了,bin log有這麼幾個特性。

  1. 追加寫,不會像 redo log 那樣被覆蓋
  2. 記錄了完整的邏輯日誌,可以利用它進行快速的資料恢復。

所以,當我們要進行資料恢復的時候可以 使用 bin log 為基礎備份出一個和原庫一樣的備庫。當我們要進行主從複製的時候,可以使用 bin log 進行 主從庫的同步

redo log是如何保證事務的永續性的

提醒一下,我這裡使用的是 “雙一配置”(即innodb_flush_log_at_trx_commit = 1 和 sync_binlog = 1 )。sync_binlog = 1的意思是 在事務每次提交的時候都會進行 bin log的持久化。而 innodb_flush_log_at_trx_commit = 1 的意思是事務提交的時候都將 redo log 持久化到磁碟。

所以這裡的雙一就是在每次事務提交的時候都會進行 redo log 和 bin log 的持久化,這兩個引數的其他配置可以去參考其他文章,這裡不做過多涉及。

這就不得不提到兩階段提交了,這時候還會牽扯到上面的另一個問題redo log的 prepare 和 commit 兩個狀態的存在意義是什麼?

兩階段提交是為了在資料庫發生 crash 之後重啟恢復能夠保證事務完整性。比如這個時候我們正在進行上面的 update 語句,然後此時資料庫宕掉了。為了你好理解我在將上面的流程圖拿過來。

你會發現,我這裡標註了三個時刻,就是我們宕機事務可能會執行到的時刻。

首先我先將規則寫在前面,你們可以對照著去理解。

  1. 如果redo log裡面的事務是完整的,也就是已經有了commit標識,則直接提交
  2. 如果redo log裡面的事務只有完整的prepare,則判斷對應的事務binlog是否存在並完整,如果存在並完整則繼續提交事務,如果不是那麼回滾事務

我們來看一下上面幾個時刻。

  • 時刻A:顯而易見,此時日誌都沒寫,東西都在記憶體中,重啟肯定會回滾(就當什麼事都沒發生)。
  • 時刻B:此時redo log 已經寫盤,但是隻是處於 prepare 狀態,如果這時候發生 crash ,那麼 bin log還沒寫 and redo log 還處於 prepare 狀態,此時事務會回滾。
  • 時刻C:此時 bin log 已經寫盤,redo log 已寫盤並處於 prepare 狀態,此時事務會根據 redo log 和 bin log 繼續提交。

為什麼會使用兩階段提交呢?

我們可以用反證法。

如果redo log在前 bin log在後,在redo log寫完之後宕機,那麼重啟之後主庫可以根據 redo log 進行資料恢復,但是這時候因為 bin log 是沒有寫的,所以如果使用 bin log 進行備份,那麼備庫會少了這一個事務。

如果bin log在前 redo log在後,在bin log寫完之後宕機,那麼就會導致後面使用 bin log做備份的時候多出這個事務。

所以如果不使用 兩階段提交 ,那麼就會出現 bin log備份出來的資料庫和原庫的資料不一致

所以redo log 結合著上面的兩階段提交就解決了,crash-safe 能力和 原庫備庫一致性。

為什麼要存在兩個日誌,只要一個不行嗎

你可能會想,如果不引入兩個日誌就沒有必要進行 兩階段提交 了,這樣豈不是快哉?!

我們可以繼續利用反證法去證明。

如果我們只有 redo log,你知道 redo log 大小是固定且是可以被覆蓋的,所以如果用來做資料備份是不可以的,因為它僅僅會記錄當前記憶體中資料頁的情況。而且 redo log是 innodb 層面的,它不是 資料庫層面的,如果當你使用的另外一個資料庫不是 以 innodb 作為儲存引擎的話,是根本進行不了同步的

如果我們只有 bin log,我們知道bin log是資料行級別且記錄的是邏輯日誌,所以是沒有“資料頁恢復”的功能的

所以,這裡我們還是需要使用 redo log 和 bin log。

redo log的 prepare 和 commit 兩個狀態的存在意義是什麼

這裡我們還得引出一個點,我們上面提到了 redo log 的落盤是在事務執行過程中。那麼,redo log究竟具體在什麼時候會進行日誌的持久化呢?

具體有三種

  1. redo log buffer佔用的空間要達到 innodb_log_buffer_size一半的時候,會有後臺執行緒主動將日誌寫盤。注意,由於這個事務並沒有提交,所以這個寫盤動作只是write,而沒有呼叫fsync,也就是隻留在了檔案系統的page cache
  2. 後臺執行緒會做每秒的輪詢將 redo log buffer write到檔案系統的page cache並呼叫 fsync 進行寫盤
  3. 並行的事務提交的時候,順帶將這個事務的redo log buffer持久化到磁碟

這裡我們提到了兩個很關鍵的詞:redo log bufferpage cache

那麼這個redo log buffer 和 page cache 又是幹什麼的呢?這裡我們需要將一下 redo log 的三種形態。如下圖

你可以想一下,一個事務會有多個 DML 語句,而每次 DML 語句都進行寫盤會進行大量的系統呼叫導致資源浪費和時間浪費,所以每次 DML 語句的時候只是會將 日誌先快取到記憶體中的 redo log buffer 中去,而最終呼叫 commit 的時候會將 redo log buffer 中的內容寫入磁碟。

而 page cache 的存在是為了加快 fsync 系統呼叫的速度,我們知道每次事務 commit 的時候都會進行兩次 fsync 呼叫(雙一配置),而主要的 redo log 一般會提早進行 write 到檔案系統緩衝中,所以這樣會加快寫盤速度。

在這裡,我放了一張加入“快取”的DML更新流程的圖。

其中 bin log cache你也可以理解為快取,而且因為bin log是邏輯日誌,所以一個事務的bin log是不能被拆開的,所以我們的 bin log cache 是存放在每個執行緒的空間裡的,相互獨立。如下圖

注意:redo log在最後只是 write 進行了寫入檔案系統的 page cache 中是因為這個時候已經可以保證 crash-safe能力了,就不需要再額外進行寫盤操作了,如果不理解可以結合上面的兩階段提交規則去理解。

所以這裡 redo log 的兩種狀態其實也是兩階段提交的重要組成部分,我們可以知道,在bin log未寫盤之前 redo log會先進行寫盤,但是這次寫盤的狀態還只是 prepare 狀態,只有在bin log 寫完之後 才會最終將狀態變為 commit,並且這裡不再進行寫盤操作,而是通過後面進行寫盤的時候順便寫入。

為什麼 WAL 技術能提高資料庫效能

我們這個時候可能還會有一個疑惑,在“雙一配置”下,每次事務的提交都需要進行兩次 fsync 系統呼叫,這樣對於資料庫的壓力會是很大的。

我們知道 WAL 技術能提高資料庫效能的一個原因是——日誌檔案是順序寫的,磁碟的順序寫要比隨機寫快很多。但是對於每次事務進行兩次系統呼叫這點,WAL 有沒有做什麼優化呢?

答案是有的,試想一下,如果存在多個事務併發的情況下,此時會出現多個事務的 redo log buffer都已經寫好,這時候 InnoDB 會使用 LSN(log sequence number)日誌邏輯序列號,LSN 是單調遞增的,用來對應 redo log的寫入點,每次寫入 length 長度的 redo log,LSN 就會增加 length。

比如此時有,三個併發事務trx1,trx2,trx3。我們可以看到 trx1 是第一個到達的,而 trx1 要進行寫盤的時候已經有三個事務在佇列中了,所以此時 trx1 去寫盤的時候帶的 LSN 就會變成 200,那麼此時進行寫盤,就會將trx1,trx2,trx3都寫入磁碟中了,這裡僅進行了一次系統呼叫。

所以這裡 WAL 技術會對一些需要系統呼叫寫盤的地方進行一些優化,儘量減少IO。對於這個問題就可以總結為兩點:

  1. 通過日誌的順序寫提高磁碟效率
  2. 通過組提交減少系統呼叫

總結

這裡我們主要介紹了在 MySQL 中 一條 DML 語句是如何執行的,redo log 、bin log又是如何和 DML 語句、事務聯絡在一起的。其中還介紹了 redo log的三種形態和兩種提交狀態,bin log的執行緒cache,LSN組提交實現等。

總的來說就是 MySQL 在進行 DML 語句的時候會先寫日誌快取(為了事務多個 DML 語句而不多次進行寫盤操作),等到事務提交的時候會進行日誌的真正落盤(“雙一配置”),其中還使用了兩階段提交加上redo log的兩種提交狀態來實現 crash-safe能力 和 redo log,bin log 的同步