6 change buffer 和 redo log
WAL 提升性能的核心機制,也是盡量減少随機讀寫,它們有啥差別呢?
插入流程
insert into t(id,k) values(id1,k1),(id2,k2);
假設目前k索引樹的狀态,查找到位置後:
- k1所在資料頁在記憶體(InnoDB buffer pool)
- k2資料頁不在記憶體
看如下流程:
帶change buffer的更新流程
圖中箭頭都是背景操作,不影響更新的響應。

該更新做了如下操作:
Page1在記憶體,直接更新記憶體
Page2不在記憶體,就在change buffer區,緩存一個“往Page2插一行記錄”的資訊
将前兩個動作記入redo log
之後事務完成。執行該更新語句成本很低,隻寫兩處記憶體,然後寫一處磁盤(前兩次操作合在一起寫了一次磁盤),還是順序寫。
處理之後的讀請求
select * from t where k in (k1, k2);
讀語句緊随更新語句,記憶體中的資料都還在,是以此時這倆讀操作就與系統表空間和 redo log 無關。
帶change buffer的讀過程
讀Page1時,直接從記憶體傳回。
WAL之後如果讀資料,是不是一定要讀盤,是不是一定要從redo log把資料更新之後才可以傳回?
其實不用。看上圖狀态,雖然磁盤上還是之前資料,但這裡直接從記憶體傳回結果,結果正确。
要讀Page2時,需把Page2從磁盤讀入記憶體,然後應用change buffer裡的記錄檔,生成一個正确版本并傳回結果。可見直到需讀Page2時,該資料頁才被讀入記憶體。
綜上,這倆機制的更新性能:
redo log 主要節省随機寫磁盤的I/O消耗(轉成順序寫)
change buffer主要節省随機讀磁盤的I/O消耗
7 總結
由于唯一索引用不了change buffer,若業務可以接受,從性能角度,優先考慮非唯一索引。
到底何時使用唯一索引
問題在于“業務可能無法確定”。本文前提是“業務代碼已經保證不會寫入重複資料”,才讨論性能問題。
如果業務不能保證或業務就是要求資料庫來做限制
沒得選,必須建立唯一索引。那本文意義的在于,如果碰上大量插入資料慢、記憶體命中率低時,多提供了一個排查思路。
“歸檔庫”場景,可考慮使用唯一索引
比如,線上資料隻需保留半年,然後曆史資料儲存在歸檔庫。此時,歸檔資料已是確定沒有唯一鍵沖突。要提高歸檔效率,可考慮把表的唯一索引改普通索引。
若某次寫入使用了change buffer,之後主機異常重新開機,是否會丢失change buffer資料
不會丢失。
雖然是隻更新記憶體,但在事務送出時,change buffer的操作也被記錄到了redo log。
是以崩潰恢複時,change buffer也能找回。
merge時是否會把資料直接寫回磁盤
merge流程
- 從磁盤讀入資料頁到記憶體(老版本資料頁)
- 從change buffer找出該資料頁的change buffer 記錄(可能多個),依次應用,得到新版資料頁
-
寫redo log
該redo log包含資料的變更和change buffer的變更
至此merge結束。
這時,資料頁和記憶體中change buffer對應磁盤位置都尚未修改,是髒頁,之後各自刷回自己實體資料,就是另外一過程。
問題思考
在構造第一個例子的過程,通過session A的配合,讓session B删除資料後又重新插入一遍資料,然後就發現explain結果中,rows字段從10001變成37000多。
而如果沒有session A的配合,隻是單獨執行delete from t 、call idata()、explain這三句話,會看到rows字段其實還是10000左右。這是什麼原因呢?
如果沒有複現,檢查
隔離級别是不是RR(Repeatable Read,可重複讀)
建立的表t是不是InnoDB引擎
為什麼經過這個操作序列,explain的結果就不對了?
delete 語句删掉了所有的資料,然後再通過call idata()插入了10萬行資料,看上去是覆寫了原來10萬行。
但session A開啟了事務并沒有送出,是以之前插入的10萬行資料是不能删除的。這樣,之前的資料每行資料都有兩個版本,舊版本是delete之前資料,新版本是标記deleted的資料。
這樣,索引a上的資料其實有兩份。
不對啊,主鍵上的資料也不能删,那沒有使用force index的語句,使用explain指令看到的掃描行數為什麼還是100000左右?(潛台詞,如果這個也翻倍,也許優化器還會認為選字段a作為索引更合适)
是的,不過這個是主鍵,主鍵是直接按照表的行數來估計的。而表的行數,優化器直接用的是show table status的值。
大家的機器如果IO能力比較差的話,做這個驗證的時候,可以把innodb_flush_log_at_trx_commit 和 sync_binlog 都設成0。
參考
https://dev.mysql.com/doc/refman/8.0/en/innodb-change-buffer.html