面試官:同窗,你能說說Mysql 緩存池嗎?面試
是CPP啊:啊,這麼難嗎,容我組織一下語言。(心裏OS:這TM還不簡單?我能給你扯半小時!)算法
面試官:能夠,給你一分鐘時間想想吧。sql
....一分鐘後....數據庫
是CPP啊:我準備好了,你可聽好,我要開始表演了。緩存
MySQL 的 innodb 存儲引擎是基於磁盤存儲的,而且是按照頁的方式進行管理的。服務器
在數據庫系統中,CPU 速度與磁盤速度之間的差距是很是大的,爲了最大可能的彌補之間的差距,提出了緩存池的概念。併發
因此緩存池,簡單來講就是一塊「內存區域」,經過內存的速度來彌補磁盤速度較慢,致使對數據庫形成性能的影響。性能
「讀操做」:學習
在數據庫中進行讀取頁的操做,首先把從磁盤讀到的頁存放在緩存池中,下一次讀取相同的頁時,首先判斷該頁是否是在緩存池中。優化
若在,稱該頁在緩存池中被命中,則直接讀取該頁,不然,仍是去讀取磁盤上的頁。
「寫操做」:
對於數據庫中頁的修改操做,首先修改在緩存池中的頁,而後在以必定的頻率刷新到磁盤,並非每次頁發生改變就刷新回磁盤,而是經過 checkpoint 的機制把頁刷新回磁盤。
能夠看到,不管是讀操做仍是寫操縱,都是對緩存池進行操做,而不是直接對磁盤進行操縱。
Buffer Pool 是一片連續的內存空間,innodb 存儲引擎是經過頁的方式對這塊內存進行管理的。
緩存池的結構以下圖:
能夠看到緩存池中包括數據頁、索引頁、插入緩存、自適應哈希索引、鎖信息、數據字段。
其中數據頁和索引頁會用掉多數內存。
「可是,innodb 是如何管理緩存池中的這麼多頁呢?」
爲了更好管理這些緩存的頁,innodb 爲每個緩存頁都建立了一些所謂的控制信息,這些控制信息包括該頁所屬的:
每一個緩存頁對應的控制信息佔用的內存大小是相同的,咱們把每一個頁對應的控制信息佔用的一塊內存稱爲一個「控制塊」。
「控制塊」和緩存頁是一一對應的,它們都被存放到 Buffer Pool 中,其中控制塊被存放到 Buffer Pool 的前邊,緩存頁被存放到 Buffer Pool 的後邊。
Buffer Pool 對應的內存空間示意圖:
「管理緩存池依賴的鏈表結構」:
當啓動 Mysql 服務器的時候,須要完成對 Buffer Pool 的初始化過程,即分配 Buffer Pool 的內存空間,把它劃分爲若干對控制塊和緩存頁,可是此時並無真正的磁盤頁被緩存到 Buffer Pool 中,以後隨着程序的運行,會不斷的有磁盤上的頁被緩存到 Buffer Pool 中。
在使用過程當中,爲了記錄哪些緩存頁是可用的,咱們把全部空閒的頁包裝成一個節點組成一個鏈表,這個鏈表能夠稱做爲 Free 鏈表(空閒鏈表)。由於剛剛完成初始化的 Buffer Pool 中全部的緩存頁都是空閒的,因此每個緩存頁都會被加入到 Free 鏈表中。
爲了方便管理 Free 鏈表,特地爲這個鏈表定義了一些「控制信息」,裏面包含鏈表的頭節點地址,尾節點地址,以及當前鏈表中節點的數量等信息。
另外會在每一個 Free 鏈表的節點中都記錄了某個「緩存頁控制塊」的地址,而每一個「緩存頁控制塊」都記錄着對應的「緩存頁地址」,因此至關於每一個 Free 鏈表節點都對應一個空閒的緩存頁。
給你們畫了個結構圖:
這圖怎麼樣,這下能看得懂了吧!
Lru 鏈表用來管理已經讀取的頁,當數據庫剛啓動時,Lru 鏈表是空的,此時頁也都放在 Free 列表中,當須要讀取數據時,會從 Free 鏈表中申請一個頁,把從放入到磁盤讀取的數據放入到申請的頁中,這個頁的集合叫作 Lru 鏈表。
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 算法的基礎上作了優化,解決了兩個問題:一、預讀失效 二、緩存池污染
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