本文主要說明 InnoDB Buffer Pool 的內部執行原理,其生效的前提是使用到了索引,如果沒有用到索引會進行全表掃描,Buffer Pool 也不會起作用。
結構
在 InnoDB 儲存引擎層維護著一個緩衝池,通過其可以避免對磁碟頻繁的IO操作。下面是其內部結構的概要圖(實際沒有這麼簡單,本文只著重說一下它的“讀”、“寫”快取)。其本質就是將磁碟上的資料頁移到記憶體中,以此來減少對磁碟資料的直接IO。
可以看到內部含有一個小區域,叫做 Change Buffer,這個是用 InnoDB 的 "寫"快取,而外面的是 InnoDB 的 “讀”快取。
讀快取
預讀
MySQL 內部一般都會使用緩衝池,而如果多次語句操作的是相鄰的記錄,那麼就會多次進行磁碟讀取,導致速度降低,所以 MySQL 一般在讀取資料時都是採用預讀方式,讀取指定資料周圍的多條資料。而在 InnoDB 引擎中的資料是以頁為單位進行儲存的,並且提出了“資料頁”概念。資料頁的結構如下,大小預設為 16K,關於資料頁這裡就不過多闡述,感興趣可以檢視原部落格。對硬碟上的資料讀取最小單位就是資料頁。
而在資料頁上面,還分為區(Extent)、段(Segment)、表空間(Tablespace),它們之間的包含關係如下圖。具體可以檢視原部落格。
InnoDB 引擎在預讀時, 有兩種預讀演算法。線性預讀和隨機預讀。
1、線性預讀(innodb_read_ahead_threshold)
選擇是否預讀下一個 Extent 的資料。有一個重要的引數 innodb_read_ahead_threshold,如果當前 Extent 中連續讀取的資料頁超過規定值,就會將下一個 Extent 的資料也讀到緩衝池中。innodb_read_ahead_threshold 的範圍是 0-64(因為一個 Extent 也就64頁)。
2、隨機預讀(innodb_random_read_ahead)
用來設定是否將當前 Extent 的剩餘頁也預讀到緩衝池中,由於這種預讀效能不穩定,所以MySQL 5.5開始預設關閉。
緩衝池的LRU演算法
InnoDB 的緩衝池資料的儲存演算法是改進版的 LRU 演算法,以此來避免了傳統 LRU 演算法的兩個問題,預讀失效和緩衝池汙染。
LRU 演算法簡單來說,如果用連結串列來實現,將最近命中(載入)的資料頁移在頭部,未使用的向後偏移,直至移除連結串列。這樣的淘汰演算法就叫做 LRU 演算法。但是其會含有前面說得兩個問題。
1、預讀失效
在磁碟上讀取資料時,可能會因為操作不當導致多個用不到的資料頁載入到緩衝池。從而導致之前經常被使用的資料頁快取被無用的資料頁擠到尾部,甚至被移出快取,那麼就會降低效能。而 InnoDB 的解決方案是將緩衝池分為兩部分,新生代和老年代,比例預設為5:3,分別儲存常用的資料頁以及不常用的資料頁,新生代位於頭部,新生代位於尾部,這兩部分都有頭部和尾部。當從磁碟的資料頁移入緩衝池中時,首先是放入老年代的頭部,然後進行篩選,使用到的資料頁會移入新生代的頭部,未使用的資料頁會隨著時間流逝而慢慢移入老年代的尾部,直至淘汰。
2、緩衝池汙染。
在處理資料頁時,如果需要對大量資料頁進行篩選(但是沒有用到),那麼還是會使大量的熱點資料頁被擠出。如 select * from student where name like '張%';name欄位包含索引,那麼在執行時雖然會先載入到老年代的頭部,但是因為每條資料都需要篩選,所以都會移入新生代頭部,導致新生代熱點資料頁被擠到老年代甚至移除。InnoDB 為了解決這個問題,使用了 "老年代停留時間視窗" 機制,這個機制是設定一個時間,如果在老年代的資料頁被呼叫後還需要去檢查它在老年代的停留時間是否達到了這個規定時間,達到了才能移入新生代頭部,否則只會移到老年代頭部。
寫快取(Change Buffer)
寫快取(Change Buffer)在5.5之前叫做 插入快取(insert Buffer),因為只支援插入的快取,在隨後版本又新增了 update、delete,所以改名 change Buffer。因為直接對磁碟進行IO操作會比較耗時,如果我們的程式在高併發的場景,同時某段時間寫操作非常多,那麼如果直接更新到磁碟上資料庫的壓力就會非常大,甚至崩潰。為了避免這種情況,可以錯開高峰期,讓資料在系統空閒時再更新到磁碟,那麼該如何實現,Change Buffer就起到這樣的作用。
執行
在更新語句進來時,首先會判斷資料頁快取中有沒有對應的資料,如果有直接更新對應的快取資料,否則將其記錄在 Change Buffer 中。隨後(不管前面是哪種情況都會執行)再將這條sql依次寫入 redo log(關於這裡有點疑問,網上很多部落格說 redo log 記錄的是具體對哪個資料頁進行什麼修改,但是如果不是通過資料頁快取更新的,那麼怎麼知道是對哪個資料頁進行操作的呢?所以我認為在通過資料頁快取更新的是資料頁更新,否則記錄的就是sql,後面就直接用sql來代替)、bin log(Server 層的日誌,所有執行引擎都可以用,而 redo log 是InnoDB內部維護的,bin log 一般用於主從複製)。
redo log落盤的時機
將日誌中的sql更新到硬碟上的操作叫做“落盤(merge)”。
1、mysql系統後臺會定期落盤
2、查詢 redo log中sql操作過的資料時需要先落盤
3、mysql 正常關閉時
4、redo log 滿了時(redo log 是固定大小的,採用迴圈寫)
Change Buffer 適用場景
1、更新後立刻需要讀取該資料場景少。因為讀取更新過的資料需要先落盤,那麼 Change Buffer 存在的意義就沒有了,同時還增加了redo log 寫入的成本。
2、非唯一索引,如果使用的是唯一索引進行查詢,那麼操作的資料需要進行唯一性檢查,所以需要將相應資料頁先載入到緩衝池中,然後再判斷,更新,過程中不會用到 Change Buffer。
寫入redo log不也是磁碟資料IO麼?為什麼就比直接更新到磁碟上效率高?
使用 redo log 只是將操作儲存進去,而更新到磁碟資料則是需要先讀操作查詢 B+ 樹,找到資料後再進行寫操作。
相關引數
Buffer Pool :
1、innodb_buffer_pool_size:緩衝池大小,在記憶體足夠的條件下,越大越好。
2、innodb_old_blocks_pct:老年代佔整個LRU鏈長度的比例,預設是37,即整個LRU的新生代和老年代長度比例是63:37。(如果配置是100就變成普通的LRU了)
3、innodb_old_blocks_time:老年代停留時間視窗,單位是毫秒,預設是1000,即同時滿足“被訪問”與“在老年代停留時間超過1秒”兩個條件,才會被插入到新生代頭部。
Change Buffer:
1、innodb_change_buffer_max_size:
配置寫緩衝的大小,佔整個緩衝池的比例,預設值是25%,最大值是50%。
畫外音:寫多讀少的業務,才需要調大這個值,讀多寫少的業務,25%其實也多了。
2、innodb_change_buffering:
介紹:配置哪些寫操作啟用寫緩衝,可以設定成all/none/inserts/deletes等。
參考部落格:
https://blog.csdn.net/mashaokang1314/article/details/109716569
https://blog.csdn.net/fu_zhongyuan/article/details/90244503