在《MySQL 學習總結 之 InnoDB 存儲引擎的架構設計》中,咱們就講到,緩衝池是 InnoDB 存儲引擎中最重要的組件。由於爲了提升 MySQL 的併發性能,使用到的數據都會緩存在緩衝池中,而後全部的增刪改查操做都將在緩衝池中執行。java
經過這種方式,保證每一個更新請求,儘可能就是只更新內存,而後往磁盤順序寫日誌文件。mysql
更新內存的性能是極高的,而後順序寫磁盤上的日誌文件的性能也是比較高的,由於順序寫磁盤文件,他的性能要遠高於隨機讀寫磁盤文件。sql
正由於緩衝池的重要性,因此咱們必須比較深刻地去理解和研究其中的原理和機制。固然了,我這裏仍是比較大白話的介紹緩衝池這個組件的原理,比較適合你們比較總體和從宏觀上去了解,若是須要更加深刻的,建議你們去看 MySQL 的官方文檔,或者是相關技術書籍。數據庫
緩衝池(Buffer Pool)的默認大小爲 128M,可經過 innodb_buffer_pool_size 參數來配置。緩存
當 SQL 執行時,用到的相關表的數據行,會將這些數據行都緩存到 Buffer Pool
中。數據結構
可是咱們能夠想象一下,若是像上面的機制那麼簡單,那麼若是是分頁的話,不斷地查詢就要不斷地將磁盤文件中數據頁的數據緩存到 Buffer Pool
中了,那麼這時候緩存池這個機制就顯得沒什麼用了,每次查詢仍是會有一次或者屢次的磁盤IO。架構
可是怎麼緩存呢?併發
咱們先了解一下數據頁這個概念。它是 MySQL 抽象出來的數據單位,磁盤文件中就是存放了不少數據頁,每一個數據頁裏存放了不少行數據。性能
默認狀況下,數據頁的大小是 16kb。學習
因此對應的,在 Buffer Pool
中,也是以數據頁爲數據單位,存放着不少數據。可是咱們一般叫作緩存頁,由於 Buffer Pool
畢竟是一個緩衝池,而且裏面的數據都是從磁盤文件中緩存到內存中。
因此,默認狀況下緩存頁的大小也是 16kb,由於它和磁盤文件中數據頁是一一對應的。
因此,緩衝池和磁盤之間的數據交換的單位是數據頁,包括從磁盤中讀取數據到緩衝池和緩衝池中數據刷回磁盤中,如圖所示:
到此,咱們都知道 Buffer Pool
中是用緩存頁來緩存數據的,可是咱們怎麼知道緩存頁對應着哪一個表,對應着哪一個數據頁呢?
因此每一個緩存頁都會對應着一個描述數據塊,裏面包含數據頁所屬的表空間、數據頁的編號,緩存頁在 Buffer Pool
中的地址等等。
描述數據塊自己也是一塊數據,它的大小大概是緩存頁大小的5%左右,大概800個字節左右的大小。
描述如圖所示:
到此,咱們都知道了,Buffer Pool
是緩存數據的數據單位爲緩存頁,利用描述數據塊來標識緩存頁。
那麼,MySQL 啓動時,是如何初始化 Buffer Pool
的呢?
一、MySQL 啓動時,會根據參數 innodb_buffer_pool_size
的值來爲 Buffer Pool
分配內存區域。
二、而後會按照緩存頁的默認大小 16k 以及對應的描述數據塊的 800個字節 左右大小,在 Buffer Pool
中劃分中一個個的緩存頁和一個個的描述數據庫塊。
三、注意,此時的緩存頁和描述數據塊都是空的,畢竟纔剛啓動 MySQL 呢。
上面咱們瞭解了 Buffer Pool
在 MySQL 啓動時是如何初始化的。當 MySQL 啓動後,會不斷地有 SQL 請求進來,此時空先的緩存頁就會不斷地被使用。
那麼, Buffer Pool
怎麼知道哪些緩存頁是空閒的呢?
free 鏈表,它是一個雙向鏈表,鏈表的每一個節點就是一個個空閒的緩存頁對應的描述數據塊。
他自己其實就是由 Buffer Pool
裏的描述數據塊組成的,你能夠認爲是每一個描述數據塊裏都有兩個指針,一個是 free_pre 指針,一個是 free_next 指針,分別指向本身的上一個 free 鏈表的節點,以及下一個 free 鏈表的節點。
經過 Buffer Pool
中的描述數據塊的 free_pre 和 free_next 兩個指針,就能夠把全部的描述數據塊串成一個 free 鏈表。
下面咱們能夠用僞代碼來描述一下 free 鏈表中描述數據塊節點的數據結構:
DescriptionDataBlock{ block_id = block1; free_pre = null; free_next = block2; }
free 鏈表有一個基礎節點,他會引用鏈表的頭節點和尾節點,裏面還存儲了鏈表中有多少個描述數據塊的節點,也就是有多少個空閒的緩存頁。
下面咱們也用僞代碼來描述一下基礎節點的數據結構:
FreeListBaseNode{ start = block01; end = block03; count = 2; }
到此,free 鏈表就介紹完了。上面咱們也介紹了 MySQL 啓動時 Buffer Pool
的初始流程,接下來,我會將結合剛介紹完的 free 鏈表,講解一下 SQL 進來時,磁盤數據頁讀取到 Buffer Pool
的緩存頁的過程。可是,咱們先要了解一下一個新概念:數據頁緩存哈希表,它的 key 是表空間+數據頁號,而 value 是對應緩存頁的地址。
描述如圖所示:
Buffer Pool
的緩存頁的過程一、首先,SQL 進來時,判斷數據對應的數據頁可否在 數據頁緩存哈希表裏 找到對應的緩存頁。
二、若是找到,將直接在 Buffer Pool
中進行增刪改查。
三、若是找不到,則從 free 鏈表中找到一個空閒的緩存頁,而後從磁盤文件中讀取對應的數據頁的數據到緩存頁中,而且將數據頁的信息和緩存頁的地址寫入到對應的描述數據塊中,而後修改相關的描述數據塊的 free_pre 指針和 free_next 指針,將使用了的描述數據塊從 free 鏈表中移除。記得,還要在數據頁緩存哈希表中寫入對應的 key-value 對。最後也是在 Buffer Pool
中進行增刪改查。
咱們都知道 SQL 的增刪改查都在 Buffer Pool
中執行,慢慢地,Buffer Pool
中的緩存頁由於不斷被修改而致使和磁盤文件中的數據不一致了,也就是 Buffer Pool
中會有不少個髒頁,髒頁裏面不少髒數據。
因此,MySQL 會有一條後臺線程,定時地將 Buffer Pool
中的髒頁刷回到磁盤文件中。
可是,後臺線程怎麼知道哪些緩存頁是髒頁呢,不可能將所有的緩存頁都往磁盤中刷吧,這會致使 MySQL 暫停一段時間。
咱們引入一個和 free 鏈表相似的 flush 鏈表。他的本質也是經過緩存頁的描述數據塊中的兩個指針,讓修改過的緩存頁的描述數據塊能串成一個雙向鏈表,這兩指針你們能夠認爲是 flush_pre 指針和 flush_next 指針。
下面我用僞代碼來描述一下:
DescriptionDataBlock{ block_id = block1; // free 鏈表的 free_pre = null; free_next = null; // flush 鏈表的 flush_pre = null; flush_next = block2; }
flush 鏈表也有對應的基礎節點,也是包含鏈表的頭節點和尾節點,還有就是修改過的緩存頁的數量。
FlushListBaseNode{ start = block1; end = block2; count = 2; }
到這裏,咱們都知道,SQL 的增刪改都會使得緩存頁變爲髒頁,此時會修改髒頁對應的描述數據塊的 flush_pre 指針和 flush_next 指針,使得描述數據塊加入到 flush 鏈表中,以後 MySQL 的後臺線程就能夠將這個髒頁刷回到磁盤中。
描述如圖所示:
咱們都知道,當加載磁盤中的數據頁到緩存中時,會從 free 鏈表找到空閒的緩存頁,而後將數據加載到緩存頁裏。
可是緩存頁總會有用完的時候,此時須要淘汰一下緩存頁,將它刷入磁盤中,而後清空。
那麼會選擇誰淘汰呢?
那麼一定會淘汰緩存命中率低的緩存頁。
什麼叫緩存命中率低:假如你有100次請求,有30次請求都是查詢和修改緩存頁一,直接操做緩存而不須要從磁盤加載,這就是緩存命中率高。而緩存頁二自加載到 Buffer Pool
後,只被查詢和修改過一次,以後的100次請求中甚至沒有一次是查詢和修改它的,這就是緩存命中率低了,由於大部分請求都是操做其餘緩存頁,甚至要從磁盤中加載。
InnoDB 存儲引擎是利用 lru 鏈表完成上面的緩存命中率的。lru 就是 Least Recently Used,最近最少使用的意思。
ps:lru 鏈表也是相似於 free 鏈表和 flush 鏈表的數據結構。
當有磁盤數據頁加載數據到緩存頁時,會將緩存頁對應的描述數據塊放入 lru 鏈表的頭部;後續只要查詢或者修改了緩存頁的數據,也會將對應描述數據塊移到 lru 鏈表的頭部去。
此時,lru 鏈表尾部的描述數據塊對應的緩存頁,一定是命中率最低的,也就是使用最少的緩存頁,因此優先被淘汰的確定是它。
lru 鏈表的使用固然不會像上面的那麼簡單。
由於 MySQL 爲了提升性能,提供了一個機制:預讀機制。
當你從磁盤上加載一個數據頁的時候,他可能會連帶着把這個數據頁相鄰的其餘數據頁,也加載到緩存裏去。這個機制會帶來這麼一個問題:連帶的數據頁可能在後面的查詢或者修改中,並不會用到,可是它們卻在 lru 鏈表的頭部。
什麼意思?
那就是,原本常常被用到的緩存頁被壓到 lru 鏈表的尾部去了,若是此時須要淘汰緩存頁,命中率高的緩存頁反而被淘汰掉了!
固然了,全表掃描也會帶來一樣的問題。
全表掃描會將表裏全部的數據一次性加載到 Buffer Pool
來,可是卻有不少數據在以後都不會用到。
什麼是冷熱分離?
簡單點,就是將命中率高的數據和命中率低的數據分開,分紅兩塊區域。
InnoDB 存儲引擎就是利用冷熱數據分離方案來解決上面的問題:將 lru 鏈表分爲兩部分,一部分是熱數據區域鏈表,一部分是冷數據區域鏈表。
lru 鏈表的頭節點指向熱數據區域的鏈表頭節點,lru 鏈表的尾節點指向冷數據區域的鏈表尾節點。
描述如圖所示:
冷熱分離的比例
由參數 innodb_old_blocks
控制,默認值爲37,表示冷數據佔全部數據的37%。
冷熱分離的原理
磁盤中的數據頁第一次加載到緩存頁時,對應的描述數據塊放到冷數據區域的鏈表頭部,而後在 1s 後,若是再次訪問這個緩存頁,纔會將緩存頁對應的描述數據塊移動到熱數據區域的鏈表頭部去。
這個 1s 由參數 innodb_old_blocks_time
指定,默認值是 1000 毫秒。
熱數據區的優化
冷數據區的緩存頁是在 1s 後再被訪問到就移動到熱數據區的鏈表頭部。那麼熱數據區域的規則呢,是否是隻要被訪問就會移動到熱數據區域的鏈表頭部。
固然不是了。你們能夠想一下,能留在熱數據區域的緩存頁,證實都是緩存命中率比較高的,會常常被訪問到。若是每一個緩存頁被訪問都移動到鏈表頭部,那這個操做將會很是的頻繁。
因此 InnoDB 存儲引擎作了一個優化,只有在熱數據區域的後 3/4 的緩存頁被訪問了,纔會移動到鏈表頭部;若是是熱數據區域的前 1/4 的緩存頁被訪問到,它是不會被移動到鏈表頭部去的。
lru 鏈表尾部的緩存頁什麼時候刷入磁盤
當 free 鏈表爲空了,此時須要將數據頁加載到緩衝池裏,就會 lru 鏈表的冷數據區域尾部的緩存頁刷入磁盤,而後清空,再加載數據頁的數據。
一條後臺線程,運行一個定時任務,定時將 lru 鏈表的冷數據區域的尾部的一些緩存頁刷入磁盤,而後清空,最後把他們對應的描述數據塊加入到 free 鏈表中去。
固然了,除了 lru 鏈表尾部的緩存頁會被刷入磁盤,還有的就是 flush 鏈表的緩存頁。
後臺線程同時也會在 MySQL 不繁忙的時候,將 flush 鏈表中的緩存頁刷入磁盤中,這些緩存頁的描述數據塊會從 lru 鏈表和 flush 鏈表中移除,並加入到 free 鏈表中。
到此,我已經將緩衝池 Buffer Pool
介紹完畢了。
下面簡單總結一下 Buffer Pool
從初始化到使用的整個流程。
一、MySQL 啓動時會根據分配指定大小內存給 Buffer Pool
,而且會建立一個個描述數據塊和緩存頁。
二、SQL 進來時,首先會根據數據的表空間和數據頁編號查詢 數據頁緩存哈希表 中是否有對應的緩存頁。
三、若是有對應的緩存頁,則直接在 Buffer Pool
中執行。
四、若是沒有,則檢查 free 鏈表看看有沒有空閒的緩存頁。
五、若是有空閒的緩存頁,則從磁盤中加載對應的數據頁,而後將描述數據塊從 free 鏈表中移除,而且加入到 lru 鏈表的冷數據區域的鏈表頭部。後面若是被修改了,還須要加入到 flush 鏈表中。
六、若是沒有空閒的緩存頁,則將 lru 鏈表的冷數據區域的鏈表尾部的緩存頁刷回磁盤,而後清空,接着將數據頁的數據加載到緩存頁中,而且描述數據塊會加入到 lru 鏈表的冷數據區域的鏈表頭部。後面若是被修改了,還須要加入到 flush 鏈表中。
七、5或者6後,就接着在 Buffer Pool
中執行增刪改查。
注意:5和6中,緩存頁加入到冷數據區域的鏈表頭部後,若是在 1s 後被訪問,則將入到熱數據區域的鏈表頭部。
八、最後,就是描述數據塊隨着 SQL 語句的執行不斷地在 free 鏈表、flush 鏈表和 lru 鏈表中移動了。