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