前言
面試官:同學,你能說說Mysql 快取池嗎?
是CPP啊:啊,這么難嗎,容我組織一下語言,(內心OS:這TM還不簡單?我能給你扯半小時!)
面試官:可以,給你一分鐘時間想一想吧,
....一分鐘后....
是CPP啊:我準備好了,你可聽好,我要開始表演了,
為什么要有快取池?
MySQL 的 innodb 存盤引擎是基于磁盤存盤的,并且是按照頁的方式進行管理的,
在資料庫系統中,CPU 速度與磁盤速度之間的差距是非常大的,為了最大可能的彌補之間的差距,提出了快取池的概念,
所以快取池,簡單來說就是一塊「記憶體區域」,通過記憶體的速度來彌補磁盤速度較慢,導致對資料庫造成性能的影響,
快取池的基本原理
「讀操作」:
在資料庫中進行讀取頁的操作,首先把從磁盤讀到的頁存放在快取池中,下一次讀取相同的頁時,首先判斷該頁是不是在快取池中,
若在,稱該頁在快取池中被命中,則直接讀取該頁,否則,還是去讀取磁盤上的頁,
「寫操作」:
對于資料庫中頁的修改操作,首先修改在快取池中的頁,然后在以一定的頻率重繪到磁盤,并不是每次頁發生改變就重繪回磁盤,而是通過 checkpoint 的機制把頁重繪回磁盤,
可以看到,無論是讀操作還是寫操縱,都是對快取池進行操作,而不是直接對磁盤進行操縱,
快取池結構
Buffer Pool 是一片連續的記憶體空間,innodb 存盤引擎是通過頁的方式對這塊記憶體進行管理的,
快取池的結構如下圖:
可以看到快取池中包括資料頁、索引頁、插入快取、自適應哈希索引、鎖資訊、資料欄位,
其中資料頁和索引頁會用掉多數記憶體,
「但是,innodb 是如何管理快取池中的這么多頁呢?」
為了更好管理這些快取的頁,innodb 為每一個快取頁都創建了一些所謂的控制資訊,這些控制資訊包括該頁所屬的:
- 表空間編號(sapce id)
- 頁號(page numeber)
- 頁 在 buffer Pool 的地址
- 一些鎖資訊以及 LSN 資訊日志序列號
- 其他控制資訊
每個快取頁對應的控制資訊占用的記憶體大小是相同的,我們把每個頁對應的控制資訊占用的一塊記憶體稱為一個「控制塊」,
「控制塊」和快取頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,快取頁被存放到 Buffer Pool 的后邊,
Buffer Pool 對應的記憶體空間示意圖:
快取池引數設定
- innodb_buffer_pool_size:快取池的大小最多應設定為物理記憶體的 80%
- innodb_buffer_pool_instance:設定有多少個快取池,通常建議把快取池個數設定為 CPU 的個數,多個快取池可以減少資料庫內部的資源競爭,增加資料庫并發訪問的能力
- innodb_old_blocks_pct:老生代占整個 LRU 的鏈長比例,默認是 3:7
- innodb_old_blocks_time:老生代停留時間視窗,單位是毫秒,默認是 1000,即同時滿足“被訪問”與“在老生代停留時間超過 1 秒”兩個條件,才會被插入到新生代頭部
快取池管理
「管理快取池依賴的鏈表結構」:
Free 鏈表
當啟動 Mysql 服務器的時候,需要完成對 Buffer Pool 的初始化程序,即分配 Buffer Pool 的記憶體空間,把它劃分為若干對控制塊和快取頁,但是此時并沒有真正的磁盤頁被快取到 Buffer Pool 中,之后隨著程式的運行,會不斷的有磁盤上的頁被快取到 Buffer Pool 中,
在使用程序中,為了記錄哪些快取頁是可用的,我們把所有空閑的頁包裝成一個節點組成一個鏈表,這個鏈表可以稱作為 Free 鏈表(空閑鏈表),因為剛剛完成初始化的 Buffer Pool 中所有的快取頁都是空閑的,所以每一個快取頁都會被加入到 Free 鏈表中,
為了方便管理 Free 鏈表,特意為這個鏈表定義了一些「控制資訊」,里面包含鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等資訊,
另外會在每個 Free 鏈表的節點中都記錄了某個「快取頁控制塊」的地址,而每個「快取頁控制塊」都記錄著對應的「快取頁地址」,所以相當于每個 Free 鏈表節點都對應一個空閑的快取頁,
給大家畫了個結構圖:
這圖怎么樣,這下能看得懂了吧!
2、Lru 鏈表
Lru 鏈表用來管理已經讀取的頁,當資料庫剛啟動時,Lru 鏈表是空的,此時頁也都放在 Free 串列中,當需要讀取資料時,會從 Free 鏈表中申請一個頁,把從放入到磁盤讀取的資料放入到申請的頁中,這個頁的集合叫做 Lru 鏈表,
3、Flush 鏈表
Flush 鏈表用來管理被修改的頁,Buffer Pool 中被修改的頁也被稱之為「臟頁」,臟頁既存在于 Lru 鏈表中,也存在于 Flush 鏈表中,Flush 鏈表中存的是一個指向 Lru 鏈表中具體資料的指標,
因此只有 Lru 鏈表中的頁第一次被修改時,對應的指標才會存入到 Flush 中,若之后再修改這個頁,則是直接更新 Lru 鏈表中的頁對應的資料,
這三者之間是這么個關系:
讀操作
Buffer Pool 一個最主要的功能是「加速讀」,加速讀是當需要訪問一個資料頁面的時候,如果這個頁面已經在快取池中,那么就不再需要訪問磁盤,直接從緩沖池中就能獲取這個頁面的內容,當我們需要訪問某個頁中的資料時,就會把該頁加載到 Buffer Pool 中,如果該頁已經在 Buffer Pool 中的話直接使用就可以了,
問題:那么如何快速查找在 Buffer Pool 中的頁呢?
為了避免查詢資料頁時掃描 Lru,其實是根據表空間號 + 頁號來定位一個頁的,
也就相當于表空間號 + 頁號是一個 key,快取頁就是對應的 value,
用表空間號 + 頁號作為 key,快取頁作為 value 創建一個哈希表,在需要訪問某個頁的資料時,先從哈希表中根據表空間號 + 頁號看看有沒有對應的快取頁,
如果有,直接使用該快取頁就好,
如果沒有,那就從 Free 鏈表中選一個空閑的快取頁,然后把磁盤中對應的頁加載到該快取頁的位置,
每當需要從磁盤中加載一個頁到 Buffer Pool 中時,就從 Free 鏈表中取一個空閑的快取頁,并且把該快取頁對應的控制塊的資訊填上,然后把該快取頁對應的 Free 鏈表節點從鏈表中移除,表示該快取頁已經被使用了,并且把該頁寫入 Lru 鏈表,
在初始化的時候,Buffer pool 中所有的頁都是空閑頁,需要讀資料時,就會從 Free 鏈表中申請頁,但是物理記憶體不可能無限增大,資料庫的資料卻是在不停增大的,所以 Free 鏈表的頁是會用完的,
因此需要考慮把已經快取的頁從 Buffer pool 中洗掉一部分,進而需要考慮如何洗掉及洗掉哪些已經快取的頁,假設一共訪問了 n 次頁,那么被訪問的頁在快取中的次數除以 n 就是快取命中率,快取命中率越高,和磁盤的 IO 互動也就越少 ,
為了提高快取命中率,InnoDB 在傳統 Lru 演算法的基礎上做了優化,解決了兩個問題:1、預讀失效 2、快取池污染
寫操作
Buffer pool 另一個主要的功能是「加速寫」,即當需要修改一個頁面的時候,先將這個頁面在緩沖池中進行修改,記下相關的重做日志,這個頁面的修改就算已經完成了,
被修改的頁面真正重繪到磁盤,這個是后臺重繪執行緒來完成的,前面頁面更新是在快取池中先進行的,那它就和磁盤上的頁不一致了,這樣的快取頁被稱為臟頁(dirty page),
問題:這些被修改的頁面什么時候重繪到磁盤?以什么樣的順序重繪到磁盤?
最簡單的做法就是每發生一次修改就立即同步到磁盤上對應的頁上,但是頻繁的往磁盤中寫資料會嚴重的影響程式的性能,所以每次修改快取頁后,不能立即把修改同步到磁盤上,而是在未來的某個時間點進行同步,由后臺重繪執行緒依次重繪到磁盤,實作修改落地到磁盤,
但是如果不立即同步到磁盤的話,那之后再同步的時候如何判斷 Buffer Pool 中哪些頁是臟頁,哪些頁從來沒被修改過呢?
InnoDB 并沒有一次性把所有的快取頁都同步到磁盤上,InnoDB 創建一個存盤臟頁的鏈表,凡是在 Lru 鏈表中被修改過的頁都需要加入這個鏈表中,因為這個鏈表中的頁都是需要被重繪到磁盤上的,所以這個鏈表也叫 Flush 鏈表,鏈表的構造和 Free 鏈表一致,
這里的臟頁修改指的此頁被加載進 Buffer Pool 后第一次被修改,只有第一次被修改時才需要加入 Flush 鏈表,對于已經存在在 Flush 鏈表中的頁,如果這個頁被再次修改就不會再放到 Flush 鏈表,
需要注意,臟頁資料實際還在 Lru 鏈表中,而 Flush 鏈表中的臟頁記錄只是通過指標指向 Lru 鏈表中的臟頁,并且在 Flush 鏈表中的臟頁是根據 oldest_lsn(這個值表示這個頁第一次被更改時的 lsn 號,對應值 oldest_modification,每個頁頭部記錄)進行排序重繪到磁盤的,值越小表示要最先被重繪,避免資料不一致,
最后
是cpp呀:面試官,我就先說這些吧!
面試官:行,回答的還湊合,(內心OS:這小子知道的還挺多啊!)
是cpp呀:我可謝謝您嘞!喜歡的朋友記得關注,看下圖了解更多噢~有學習驚喜O

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/247331.html
標籤:MySQL
下一篇:postgresql 角色權限
