Buffer Pool緩存頁不夠時,如何淘汰緩存? Buffer Pool緩存頁不夠時,如何淘汰緩存?

Buffer Pool緩存頁不夠時,如何淘汰緩存?

若BP緩存頁不夠了,咋辦?

執行CRUD都會將磁盤數據頁加載到緩存頁,那在加載數據到緩存頁時,必然是要加載到空閒緩存頁,所以必須要從free中找個空閒緩存頁,然後把磁盤數據頁加載到該空閒緩存頁

隨着不斷將磁盤數據頁加載到空閒緩存頁,free中的空閒緩存頁會越來越少。最終耗盡free中的空閒緩存頁。這時,還要加載數據頁到一個空閒緩存頁時,MySQL 該何去何從?

若所有緩存頁都有數據了,那就無法再從磁盤加載新數據頁到緩存頁了,則只能淘汰一些緩存頁:把一個緩存頁裏被修改過的數據,刷到磁盤的數據頁,然後該緩存頁就能被清空, 變回空閒頁。然後就能將磁盤的新數據頁加載到這剛騰出的空閒頁:

那應該把哪個倒黴的緩存頁的數據刷盤呢?

緩存命中率

現有兩個緩存頁:

  • 一個緩存頁的數據,經常被修改和查詢,都可以操作緩存,不需要從磁盤加載數據,這那緩存命中率就很高。這種高級員工就是啥髒活累活,都會接受。
  • 另一個緩存頁裏的數據,剛從磁盤加載到緩存頁後,被修改和查詢過1次,之後100次請求再沒有一次是修改和查詢該緩存頁數據的,那這緩存命中率就有點低了,因爲大部分請求還是走磁盤查詢數據,他們要操作的數據不在緩存。這種高級員工啥事都幹不了,都還得交給低級員工們幹事。

很顯然,作爲領導的你,肯定想把第二個員工裁了吧?

引入LRU,判斷哪些緩存頁不常用

如何知曉哪些緩存頁經常被訪問,哪些緩存頁很少被訪問?

這就需要LRU,Least Recently Used,最近最少使用。這樣當緩存頁需空出一個刷盤時,通過LRU鏈表,就能知道最近最少被使用的緩存頁。

LRU工作原理

假設從磁盤加載一個數據頁到緩存頁時,就將該緩存頁的描述信息塊放入LRU鏈表頭部,那麼只要有數據的緩存頁,他都會在LRU裏,最近被加載數據的緩存頁,都會放到LRU鏈表頭部。

假設某緩存頁的描述信息塊本在LRU鏈尾,後續你只要查詢或修改了該緩存頁數據,也要將這緩存頁移到LRU鏈頭,即最近被訪問過的緩存頁,一定在LRU鏈頭

如此,當無空閒緩存頁時候,就能輕易找出最近最少被訪問的緩存頁去刷盤,即LRU鏈尾的緩存頁,將其刷盤,然後把你需要的磁盤數據頁加載到這剛空出的緩存頁。

表和行等概念和表空間、數據頁的關係

  • 表、列和行,都是邏輯概念,我們只關注DB裏有一個表,表裏有幾個字段,有多少行,但是這些表裏的數據
  • 表空間、數據頁等是物理概念,在物理層面,表裏的數據都放在一個表空間,表空間由一堆磁盤上的數據文件組成,這些數據文件裏都存放了表中的數據,這些數據由一個個數據頁組織起來

但這樣的LRU實際運行時會有問題。

預讀

當你從磁盤加載一個數據頁時,他可 能會連帶着把該數據頁相鄰的其他數據頁,也加載到緩存。

現有兩個空閒緩存頁,加載一個數據頁時,連帶着把他的一個相鄰數據頁也加載到緩存,正好每個數據頁放入一個空閒緩存頁!

然後呢?實際上只有一個緩存頁被訪問,另外一個通過預讀機制加載的緩存頁,其實無人問津,此時這倆緩存頁可都在LRU鏈表前邊:

這時,若無空閒頁了,要加載新數據頁,就得從LRU鏈表的尾部將“最近最少使用的緩存頁”取出,刷入磁盤,就空出一個緩存頁了。

若選擇將上圖中LRU尾部那個緩存頁刷盤,然後清空,合理嗎?

他可是之前一直頻繁被訪問呀,只是這一瞬間,被新加載進的兩個緩存頁給佔了LRU鏈表前面的位置,尤是第二個緩存頁,居然還是通過預讀加載來的,其實根本無人訪問!而這時將LRU鏈表尾部緩存頁刷盤,肯定不合理,最合理的反而是將那LRU鏈表第二個通過預讀機制加載進的緩存頁給淘汰。

MySQL預讀觸發時機

  1. 參數innodb_read_ahead_threshold默認56,即若順序訪問了一個區裏的多個數據頁,訪問的數據頁數量超過閾值,就會觸發預讀,將下個相鄰區中的所有數據頁都加載到緩 存

  2. 若BP裏緩存了一個區裏的13個連續的數據頁,而且這些數據頁都是比較頻繁會被訪問的,此時直接觸發預讀,把這個區裏的其他的數據頁都加載到緩存裏去。該機制通過參數innodb_random_read_ahead控制,默認OFF關閉。

所以默認主要第一個規則可能觸發預讀,一下將很多相鄰區裏的數據頁加載進緩存,這些緩存頁若突然都放在LRU鏈表前面,且他們其實並沒啥人訪問,就會如上圖,導致本就在緩存裏的一些頻繁被訪問的緩存頁卻在LRU鏈尾。後續一旦要淘汰緩存頁,就會將鏈尾的一些頻繁被訪問的緩存頁給淘汰!

全表掃描

如:

SELECT * FROM xxx

一下子就將表裏所有數據頁都從磁盤加載到BP。這時他可能會一下子就把這個表的所有數據頁都裝入各緩存頁。此時可能LRU鏈表中排在前面的一大串緩存頁,都是全表掃描加載進來的。若此次全表掃描後,後續幾乎沒用到這個表裏的數據呢?此時LRU鏈尾可能全都是之前一直被頻繁訪問的那些緩存頁!

然後當需要淘汰緩存頁時,就會將LRU鏈表尾部一直被頻繁訪問的緩存頁給淘汰掉了,而留下之前全表掃描加載進來的大量的不經常訪問的緩存頁。

爲何MySQL設計預讀機制,爲何有時要把相鄰的一些數據頁一次性讀入到Buffer Pool緩存?

爲提升性能。假設你讀取了數據頁01到緩存頁裏去,那接下來有可能會接着順序讀取數據頁01相鄰的數據頁02到緩存頁,是不是可能在讀取數據頁02的時候要再次發起一次磁盤IO?

所以爲優化性能,MySQL設計了預讀機制,即若在一個區內,你順序讀取了好多數據頁,比如數據頁01~56都被你依次順序讀取了,MySQL覺得你可能接着會繼續順序讀取後面的數據頁。

此時他乾脆提前把後續一大堆數據頁(如數據頁57~72)都讀取到Buffer Pool,後續你再讀取數據頁60時,就能直接從Buffer Pool裏拿到。

但現實骨感,預讀的一大堆數據頁要是佔據LRU鏈表前面部分,然而可能這些預讀的數據頁壓根兒後續無人用,那這預讀機制對性能不增反減。

冷熱分離的LRU

於是,爲了解決前面的問題,真正MySQL採取冷熱數據分離思想改良了 LRU。

之前問題都是因爲所有緩存頁都混在一個LRU鏈表才導致的,改良版LRU鏈表拆爲熱數據、冷數據兩部分,冷熱數據比例由innodb_old_blocks_pct參數控制,默認37,即冷數據佔37%。這時的LRU鏈表:

數據頁第一次被加載到緩存時,緩存頁會被放在冷區的鏈表頭部。

冷區緩存頁何時放入熱區?

第一次被加載了數據的緩存頁都會不停移動到冷區的鏈表頭部。那爲何不放到熱區頭部呢?

你剛加載了一個數據頁到那個緩存頁,他在冷區的鏈表頭部,然後立馬(在1ms以內)就又被訪問了,但之後就再也不訪問了呢?難道這種情況也要把這緩存頁放到熱區頭部嗎?

所以MySQL設innodb_old_blocks_time參數,默認1000,即1000ms:一個數據頁被加載到緩存頁之後,在1s後,你又訪問了該緩存頁,他纔會被移到熱區的鏈表頭部。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章