MySQL重做日誌(redo log)
阿新 • • 發佈:2020-12-05
前面介紹了三種日誌:[error log](https://www.cnblogs.com/liang24/p/14072666.html)、[slow log](https://www.cnblogs.com/liang24/p/14079272.html)、[binlog](https://www.cnblogs.com/liang24/p/14084235.html),這三種都是 Server 層的。今天的 redo log 是 InnoDB引擎專有的日誌檔案。
### 為什麼要有 redo log
用個酒店掌櫃記賬的例子說明 redo log的作用。
酒店掌櫃有一個粉板,專門用來記錄客人的賒賬記錄。如果賒賬的人不多,那麼他可以把顧客名和賬目寫在板上。但如果賒賬的人多了,粉板總會有記不下的時候,這個時候掌櫃一定還有一個專門記錄賒賬的賬本
如果有人要賒賬或者還賬的話,掌櫃一般有兩種做法:
1. 直接翻開賬本記錄(直接寫磁碟)
2. 先記在粉板(redo log)上,等空閒時再記錄到賬本(磁碟)上
當生意火爆時,不停有人來要賒賬或者還賬(更新操作),如果掌櫃還是用第一種做法,由於記到賬本上需要查詢記錄(隨機讀)那就會出現大量的人(更新操作)在等待,會影響工作(阻塞)。
第二種做法,先記在粉板上,空閒時再寫回賬本。因為記粉板的速度是很快的,就能大量處理賒賬或者還賬,當掌櫃(MySQL)沒那麼忙的時候,就把粉板上的內容記到賬本上。但如果粉板(redo log)寫滿了,那這時候掌櫃(MySQL)就要停下工作,先去把粉板(redo log)的內容寫回賬本(磁碟)。
兩種做法的區別是:
1. 第一種是要找到記錄後更新,這裡涉及隨時讀,而隨時讀是很費時間的
2. 第二種是記在日誌上,這是順序寫的,而順序寫是很快的
因此MySQL採用第二種做法,當有一條記錄需要更新的時候,InnoDB引擎就會先把記錄寫到redo log(粉板)裡面,並更新記憶體,這個時候更新就算完成了。同時,InnoDB引擎會在適當的時候,將這個操作記錄更新到磁盤裡面,而這個更新往往是在系統比較空閒的時候做,這就像打烊以後掌櫃做的事。
這就是WAL(Write-Ahead Logging),先寫日誌,再寫磁碟。
與粉板類似,redo log 也是有固定大小的。比如可以配置為一組4個檔案,每個檔案的大小是1GB,那麼這塊“粉板”總共就可以記錄4GB的操作。從頭開始寫,寫到末尾就又回到開頭迴圈寫,如下面這個圖所示。
![image](https://static001.geekbang.org/resource/image/b0/9c/b075250cad8d9f6c791a52b6a600f69c.jpg)
兩個指標:
- write pos是當前記錄的位置
- checkpoint是當前要擦除的位置
有了 redo log,InnoDB 可以保證即使資料庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為 crash-safe。
### 如何寫入
下面以一條Update語句來介紹 binlog 是如何記錄的。這裡在 [binlog](https://www.cnblogs.com/liang24/p/14084235.html) 裡也有介紹過。
```mysql
mysql> update T set c=c+1 where ID=2;
```
![image](https://static001.geekbang.org/resource/image/2e/be/2e5bff4910ec189fe1ee6e2ecc7b4bbe.png)
1. 取得 ID=2 這一行(通過記憶體或磁碟讀取)
2. 這行的 c 值加1
3. 更新到記憶體
4. 寫入 redo log(處於 prepare 階段)
5. 寫 binlog
6. 提交事務(處於 commit 狀態)
#### 兩階段提交
上面的流程採用了兩階段提交,那為什麼要採用兩階段提交呢?是為了讓 binlog 和 redo log 之間的邏輯一致。
我們假設一下上面的 update 語句在執行的每個時刻,MySQL 崩潰了,看一下兩個日誌間的邏輯是如何保持一致的。
- 假設在步驟4前,MySQL崩潰重啟,那麼事務提交失敗,不會影響資料。雖然更新了記憶體,但崩潰後,記憶體會丟失。
- 假設在步驟4完成後崩潰,此時已經寫入 redo log 了,重啟後,發現 redo log 處於 prepare 階段,就不恢復。
- 假設在步驟5完成後崩潰,此時已經寫入 binlog 了,重啟後,發現 binlog 已經寫入了,就把對應的 redo log 改為 commit 狀態。
這樣就能保證 redo log 和 binlog 的邏輯一致性。
兩階段提交是跨系統維持資料邏輯一致性時常用的一個方案。
### 如何使用
這裡要介紹一下 redo log 裡非常重要的一個引數:`innodb_flush_log_at_trx_commit`。
**`innodb_flush_log_at_trx_commit=0`** 表示提交事務的時候,不立即把 redo log buffer 裡的資料刷入磁碟檔案的,而是依靠 InnoDB 的主執行緒每秒執行一次重新整理到磁碟。此時可能你提交事務了,結果 mysql 宕機了,然後此時記憶體裡的資料全部丟失。
**`innodb_flush_log_at_trx_commit=1`** 表示提交事務的時候,就必須把 redo log 從記憶體刷入到磁碟檔案裡去,只要事務提交成功,那麼 redo log 就必然在磁盤裡了。注意,因為作業系統的“延遲寫”特性,此時的刷入只是寫到了作業系統的緩衝區中,因此執行同步操作才能保證一定持久化到了硬碟中。
**`innodb_flush_log_at_trx_commit=2`** 表示提交事務的時候,把 redo 日誌寫入磁碟檔案對應的 os cache 快取裡去,而不是直接進入磁碟檔案,可能 1 秒後才會把 os cache 裡的資料寫入到磁碟檔案裡去。
#### 如何選擇
- 對資料丟失很敏感,設定為1,保證寫到磁碟上。但效能較差。
- 對資料不太敏感,設定為0或2,效能較好。但可能會丟失1秒的資料。
#### 如何檢視和設定
通過以下命令檢視當前 `innodb_flush_log_at_trx_commit` 的值是多少:
```mysql
mysql> select @@innodb_flush_log_at_trx_commit;
+----------------------------------+
| @@innodb_flush_log_at_trx_commit |
+----------------------------------+
| 1 |
+----------------------------------+
1 row in set (0.00 sec)
```
在 `/etc/mysql/my.cnf` 檔案裡設定 `innodb_flush_log_at_trx_commit` 選項:
```
#/etc/mysql/my.cnf
innodb_flush_log_at_trx_commit=1
```
修改後,重啟 MySQL 服務即可。
### redo log 與 binlog 的區別
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
- redo log 是物理日誌,記錄的是“在某個資料頁上做了什麼修改”;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯
- redo log 是迴圈寫的,空間固定會用完;binlog 是可以追加寫入的。
### 參考資料
- [02 | 日誌系統:一條SQL更新語句是如何執行的?](https://time.geekbang.org/column/article/68633)
- [12 | 為什麼我的MySQL會“抖”一下?](https://time.geekbang.org/column/article/71806)
- [15 | 答疑文章(一):日誌和索引相關問題](https://time.geekbang.org/column/articl