「極客時間23講筆記」MySQL是怎麼保證資料不丟的

主要靠

redo log

binlog

保證持久化到磁碟,就能確保 MySQL 異常重啟後,資料可以恢復。

binlog 的寫入機制

事務執行過程中,先把日誌寫到 binlog cache,事務提交的時候,再把 binlog cache 寫到 binlog 檔案中。如圖,

「極客時間23講筆記」MySQL是怎麼保證資料不丟的

可以看到,每個執行緒有自己 binlog cache,但是共用同一份 binlog 檔案。

圖中的 write,指的就是指把日誌寫入到檔案系統的 page cache,並沒有把資料持久化到磁碟,所以速度比較快。

圖中的 fsync,才是將資料持久化到磁碟的操作。一般情況下,我們認為 fsync 才佔磁碟的 IOPS。

write 和 fsync 的時機,是由引數 sync_binlog 控制的:

sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync;

sync_binlog=1 的時候,表示每次提交事務都會執行 fsync;

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

因此,在出現 IO 瓶頸的場景裡,將 sync_binlog 設定成一個比較大的值,可以提升效能。在實際的業務場景中,考慮到丟失日誌量的可控性,一般不建議將這個引數設成 0,比較常見的是將其設定為 100~1000 中的某個數值。

redo log 的寫入機制

「極客時間23講筆記」MySQL是怎麼保證資料不丟的

和binlog相似,不過redo log buffer是執行緒共享的(

為何binlog buffer不能共享?

MySQL 這麼設計的主要原因是,binlog 是不能“被打斷的”。一個事務的 binlog 必須連續寫,因此要整個事務完成後,再一起寫到檔案裡。而 redo log 並沒有這個要求,中間有生成的日誌可以寫到 redo log buffer 中。redo log buffer 中的內容還能“搭便車”,其他事務提交的時候可以被一起寫到磁碟中。)。

redo log中的資料可能存在的三種狀態:

存在 redo log buffer 中,物理上是在 MySQL 程序記憶體中,就是圖中的紅色部分;

寫到磁碟 (write),但是沒有持久化(fsync),物理上是在檔案系統的 page cache 裡面,也就是圖中的黃色部分;

持久化到磁碟,對應的是 hard disk,也就是圖中的綠色部分。

日誌寫到 redo log buffer 是很快的,wirte 到 page cache 也差不多,但是持久化到磁碟的速度就慢多了。

為了控制 redo log 的寫入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 引數,它有三種可能取值:

設定為 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ;

設定為 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁碟;

設定為 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache。

如果

innodb_flush_log_at_trx_commit 這個引數設定0

或2的時候,InnoDB也會有一個後臺執行緒,每隔 1 秒,就會把 redo log buffer 中的日誌,呼叫 write 寫到檔案系統的 page cache,然後呼叫 fsync 持久化到磁碟。(

無論這個引數設定的是0、1還是2,最終都會通過後臺執行緒刷進page cache,再由作業系統寫進磁碟。

innodb_flush_log_at_trx_commit引數詳解

兩階段提交的時候說過,時序上 redo log 先 prepare(

所謂的 redo log prepare,是“當前事務提交”的一個階段,也就是說,在事務A提交的時候,我們才會走到事務A的redo log prepare這個階段。事務A在提交前,有一部分redo log被事務B提前持久化,但是事務A還沒有進入提交階段,是無所謂“redo log prepare”的。

), 再寫 binlog,最後再把 redo log 執行commit。

如果把 innodb_flush_log_at_trx_commit 設定成 1,那麼 redo log 在 prepare 階段就要持久化一次,因為有一個崩潰恢復邏輯是要依賴於 prepare 的 redo log,再加上 binlog 來恢復的。

每秒一次後臺輪詢刷盤,再加上崩潰恢復這個邏輯,InnoDB 就認為 redo log 在 commit 的時候就不需要 fsync 了,只會 write 到檔案系統的 page cache 中就夠了

這裡說明了,redo log 狀態改為commit的時候不會進行fsync,因為只要binlog 寫磁碟成功,就算redo log 的狀態還是prepare也沒有關係會被認為事務已經執行成功,所以只需要write 到page cache就ok了,沒必要再浪費io主動去進行一次fsync。這個write動作交給後臺執行緒去執行。

)。

通常我們說 MySQL 的“雙 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都設定成 1。也就是說,一個事務完整提交前,需要等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog。

組提交(group commit)機制

你可能有一個疑問,這意味著我從 MySQL 看到的 TPS 是每秒兩萬的話,每秒就會寫四萬次磁碟。但是用工具測試出來,磁碟能力也就兩萬左右,怎麼能實現兩萬的 TPS?

這裡,我需要先和你介紹日誌邏輯序列號(log sequence number,LSN)的概念。LSN 是單調遞增的,

用來對應 redo log

的一個個

寫入點

。每次寫入長度為 length 的 redo log, LSN 的值就會加上 length。如圖,

是三個併發事務 (trx1, trx2, trx3) 在 prepare 階段,都寫完 redo log buffer,持久化到磁碟的過程,對應的 LSN 分別是 50、120 和 160。

「極客時間23講筆記」MySQL是怎麼保證資料不丟的

從圖中可以看到,

trx1 是第一個到達的,會被選為這組的 leader;

等 trx1 要開始寫盤的時候,這個組裡面已經有了三個事務,這時候 LSN 也變成了 160;

trx1 去寫盤的時候,帶的就是 LSN=160,因此等 trx1 返回時,所有 LSN 小於等於 160 的 redo log,都已經被持久化到磁碟。

這時候 trx2 和 trx3 就可以直接返回了。

所以,一次組提交裡面,組員越多,節約磁碟 IOPS 的效果越好。但如果只有單執行緒壓測,那就只能老老實實地一個事務對應一次持久化操作了。在併發更新場景下,第一個事務寫完 redo log buffer 以後,接下來這個 fsync 越晚呼叫,組員可能越多,節約 IOPS 的效果就越好。

為了讓一次 fsync 帶的組員更多,MySQL 有一個很有趣的最佳化:

拖時間。

如在兩階段提交的時候,如圖,

「極客時間23講筆記」MySQL是怎麼保證資料不丟的

圖中,實際上,寫 binlog 是分成兩步的。

先把 binlog 從 binlog cache 中寫到磁碟上的 binlog 檔案。

呼叫 fsync 持久化。

MySQL 為了讓組提交的效果更好,把 redo log 做 fsync 的時間拖到了步驟 1 之後。也就是說,上面的圖變成了這樣:

「極客時間23講筆記」MySQL是怎麼保證資料不丟的

這麼一來,binlog 也可以組提交了。在執行圖中第 4 步把 binlog fsync 到磁碟時,如果有多個事務的 binlog 已經寫完了,也是一起持久化的,這樣也可以減少 IOPS 的消耗。

事務執行期間,還沒到提交階段,如果發生 crash 的話,redo log 肯定丟了,這會不會導致主備不一致呢?

不會。因為這時候 binlog 也還在 binlog cache 裡,沒發給備庫。crash 以後 redo log 和 binlog 都沒有了,從業務角度看這個事務也沒有提交,所以資料是一致的。