本篇已收錄在 MySQL 是怎樣運行的 學習筆記系列html
緩存的重要性
InnoDB存儲引擎在處理客戶端的請求時,當須要訪問某個頁的數據時,就會把完整的頁的數據所有加載到內存中,也就是說即便咱們只須要訪問一個頁的一條記錄,那也須要先把整個頁的數據加載到內存中。將整個頁加載到內存中後就能夠進行讀寫訪問了,在進行完讀寫訪問以後並不着急把該頁對應的內存空間釋放掉,而是將其緩存起來,這樣未來有請求再次訪問該頁面時,就能夠省去磁盤IO的開銷了。mysql
Buffer Pool
爲了緩存磁盤中的頁,在MySQL服務器啓動的時候就向操做系統申請了一片連續的內存,他們給這片內存起了個名,叫作Buffer Pool(中文名是緩衝池)。sql
默認狀況下Buffer Pool只有128M大小。緩存
能夠在啓動服務器的時候配置innodb_buffer_pool_size參數的值,它表示Buffer Pool的大小,就像這樣:服務器
[server]
innodb_buffer_pool_size = 268435456
其中,268435456的單位是字節,也就是我指定Buffer Pool的大小爲256M。須要注意的是,Buffer Pool也不能過小,最小值爲5M(當小於該值時會自動設置成5M)。
dom
Buffer Pool內部組成
Buffer Pool中默認的緩存頁大小和在磁盤上默認的頁大小是同樣的,都是16KB。學習
爲了更好的管理這些在Buffer Pool中的緩存頁,mysql 爲每個緩存頁都建立了一些所謂的控制信息, 包括該頁所屬的表空間編號、頁號、緩存頁在Buffer Pool中的地址、鏈表節點信息、一些鎖信息以及LSN信息spa
控制塊和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 後邊,因此整個Buffer Pool對應的內存空間看起來就是這樣的:操作系統
free 鏈表
當咱們最初啓動MySQL服務器的時候,須要完成對Buffer Pool的初始化過程,就是先向操做系統申請Buffer Pool的內存空間,而後把它劃分紅若干對控制塊和緩存頁。可是此時並無真實的磁盤頁被緩存到Buffer Pool中(由於尚未用到),以後隨着程序的運行,會不斷的有磁盤上的頁被緩存到Buffer Pool中。code
從磁盤上讀取一個頁到Buffer Pool中的時候該放到哪一個緩存頁的位置呢?或者說怎麼區分Buffer Pool中哪些緩存頁是空閒的,哪些已經被使用了呢?咱們最好在某個地方記錄一下Buffer Pool中哪些緩存頁是可用的,這個時候緩存頁對應的控制塊就派上大用場了,咱們能夠把全部空閒的緩存頁對應的控制塊做爲一個節點放到一個鏈表中,這個鏈表也能夠被稱做free鏈表(或者說空閒鏈表)。
從圖中能夠看出,咱們爲了管理好這個free鏈表,特地爲這個鏈表定義了一個基節點,裏邊兒包含着鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。這裏須要注意的是,鏈表的基節點佔用的內存空間並不包含在爲Buffer Pool申請的一大片連續內存空間以內,而是單獨申請的一塊內存空間。
緩存頁的哈希處理
前邊說過,當咱們須要訪問某個頁中的數據時,就會把該頁從磁盤加載到Buffer Pool中,若是該頁已經在Buffer Pool中的話直接使用就能夠了。那麼問題也就來了,咱們怎麼知道該頁在不在Buffer Pool中呢?難不成須要依次遍歷Buffer Pool中各個緩存頁麼?一個Buffer Pool中的緩存頁這麼多都遍歷完豈不是要累死?
咱們能夠用表空間號 + 頁號做爲key,緩存頁做爲value建立一個哈希表,在須要訪問某個頁的數據時,先從哈希表中根據表空間號 + 頁號看看有沒有對應的緩存頁,若是有,直接使用該緩存頁就好,若是沒有,那就從free鏈表中選一個空閒的緩存頁,而後把磁盤中對應的頁加載到該緩存頁的位置。
flush鏈表的管理
若是咱們修改了Buffer Pool中某個緩存頁的數據,那它就和磁盤上的頁不一致了,這樣的緩存頁也被稱爲髒頁(英文名:dirty page)。
每次修改緩存頁後,咱們並不着急當即把修改同步到磁盤上,而是在將來的某個時間點進行同步, 再建立一個存儲髒頁的鏈表,凡是修改過的緩存頁對應的控制塊都會做爲一個節點加入到一個鏈表中,由於這個鏈表節點對應的緩存頁都是須要被刷新到磁盤上的,因此也叫flush鏈表。鏈表的構造和free鏈表差很少,假設某個時間點Buffer Pool中的髒頁數量爲n,那麼對應的flush鏈表就長這樣:
LRU鏈表的管理
Buffer Pool對應的內存大小畢竟是有限的,若是須要緩存的頁佔用的內存大小超過了Buffer Pool大小, 就須要把某些舊的緩存頁從Buffer Pool中移除,而後再把新的頁放進來. 當Buffer Pool中再也不有空閒的緩存頁時,就須要淘汰掉部分最近不多使用的緩存頁。
咱們能夠再建立一個鏈表,因爲這個鏈表是爲了按照最近最少使用的原則去淘汰緩存頁的,因此這個鏈表能夠被稱爲LRU鏈表(LRU的英文全稱:Least Recently Used)。當咱們須要訪問某個頁時,能夠這樣處理LRU鏈表:
若是該頁不在Buffer Pool中,在把該頁從磁盤加載到Buffer Pool中的緩存頁時,就把該緩存頁對應的控制塊做爲節點塞到鏈表的頭部。 若是該頁已經緩存在Buffer Pool中,則直接把該頁對應的控制塊移動到LRU鏈表的頭部。
只要咱們使用到某個緩存頁,就把該緩存頁調整到LRU鏈表的頭部,這樣LRU鏈表尾部就是最近最少使用的緩存頁
劃分區域的LRU鏈表
上邊的這個簡單的LRU鏈表用了沒多長時間就發現問題了,由於存在這兩種比較尷尬的狀況:
狀況一:InnoDB 提供了一個預讀功能(英文名:read ahead)。所謂預讀,就是InnoDB認爲執行當前的請求可能以後會讀取某些頁面,就預先把它們加載到Buffer Pool中。預讀原本是個好事兒,若是預讀到Buffer Pool中的頁成功的被使用到,那就能夠極大的提升語句執行的效率。但是若是用不到呢?這些預讀的頁都會放到LRU鏈表的頭部,可是若是此時Buffer Pool的容量不太大並且不少預讀的頁面都沒有用到的話,這就會致使處在LRU鏈表尾部的一些緩存頁會很快的被淘汰掉,也就是所謂的劣幣驅逐良幣,會大大下降緩存命中率。
狀況二:有的小夥伴可能會寫一些須要掃描全表的查詢語句(好比沒有創建合適的索引或者壓根兒沒有WHERE子句的查詢)。意味着將訪問到該表所在的全部頁!假設這個表中記錄很是多的話,那該表會佔用特別多的頁,當須要訪問這些頁時,會把它們通通都加載到Buffer Pool中, 這嚴重的影響到其餘查詢對 Buffer Pool的使用,從而大大下降了緩存命中率。
由於有這兩種狀況的存在,因此 InnoDB 把這個LRU鏈表按照必定比例分紅兩截,分別是:
一部分存儲使用頻率很是高的緩存頁,因此這一部分鏈表也叫作熱數據,或者稱young區域。 另外一部分存儲使用頻率不是很高的緩存頁,因此這一部分鏈表也叫作冷數據,或者稱old區域。
查看Buffer Pool的狀態信息
SHOW ENGINE INNODB STATUS
mysql> SHOW ENGINE INNODB STATUS\G (...省略前邊的許多狀態) ---------------------- BUFFER POOL AND MEMORY ---------------------- Total memory allocated 13218349056; Dictionary memory allocated 4014231 Buffer pool size 786432 Free buffers 8174 Database pages 710576 Old database pages 262143 Modified db pages 124941 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young 6195930012, not young 78247510485 108.18 youngs/s, 226.15 non-youngs/s Pages read 2748866728, created 29217873, written 4845680877 160.77 reads/s, 3.80 creates/s, 190.16 writes/s Buffer pool hit rate 956 / 1000, young-making rate 30 / 1000 not 605 / 1000 Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 710576, unzip_LRU len: 118 I/O sum[134264]:cur[144], unzip sum[16]:cur[0] -------------- (...省略後邊的許多狀態)