前言
我們了解到buffer pool 是InnoDB獨有的一個內(nèi)存結構,之前初步了解到數(shù)據(jù)的增刪改都是在這塊內(nèi)存里面執(zhí)行。因為我們不可能直接在磁盤中對數(shù)據(jù)進行增刪改,如果對磁盤的隨機讀寫,速度會非常的慢,就更別談每秒處理上千的請求了。之前也描述過雖然是在buffer pool是在內(nèi)存中,但MySQL有非常嚴謹?shù)亩档追椒?,就算宕機了,也可以配合 redo log 來進行數(shù)據(jù)的更新和刷盤。那buffer pool究竟長什么樣子?
1. buffer pool 占用多少內(nèi)存,多少合適?
buffer pool是一個內(nèi)存數(shù)據(jù)結構,在MySQL內(nèi)部肯定是占有一定的內(nèi)存。
buffer pool的默認大小是128M,這個可以根據(jù)實際環(huán)境來進行分配。MySQL官網(wǎng)給出的建議是大小占用實際環(huán)境的80%,比如MySQL服務器是一個8核16G的機器,則可以分配12G內(nèi)存。
innodb_buffer_pool_size = 12,884,901,888

2. buffer pool 存儲的是什么樣的數(shù)據(jù)?
大家都清楚數(shù)據(jù)庫的核心數(shù)據(jù)模型是表+字段+行的概念,也就是說數(shù)據(jù)庫有很多個表,表里面有很多的字段,每個字段都有對應的值,一行一行的數(shù)據(jù)構成了表。那有沒有可能buffer pool儲存的是一行一行的數(shù)據(jù)?
答案不是的。實際上MySQL對數(shù)據(jù)抽象出了一個數(shù)據(jù)頁的概念,他把很多行數(shù)據(jù)放在了一個數(shù)據(jù)頁里,也即是說磁盤文件里有很多的數(shù)據(jù)頁,每一個數(shù)據(jù)頁里放了很多行數(shù)據(jù)。

所以實際上,更新一行數(shù)據(jù),數(shù)據(jù)庫找到這行數(shù)據(jù)所在的數(shù)據(jù)頁,然后從磁盤中把數(shù)據(jù)頁加載到buffer pool中。也就是說,buffer pool存放的是一個一個的數(shù)據(jù)頁。

3. 磁盤中的數(shù)據(jù)頁與buffer pool中的緩存頁如何對應起來?
默認情況下,一個數(shù)據(jù)頁的大小為16kb,,包含了16kb的內(nèi)容,而buffer pool中緩存頁跟數(shù)據(jù)頁是一一對應的,即是說buffer pool中緩存頁的大小也為16kb。

而每個緩存頁在buffer pool中都有對應的描述信息。描述信息也是一個數(shù)據(jù)塊,相當于緩存頁大小的5%,里面描述了該數(shù)據(jù)頁所屬的表空間、數(shù)據(jù)頁的編號、這個緩存頁在buffer pool中的地址等信息。每個描述信息放在buffer pool最前面,各個緩存頁放在后面。

4. 初始化buffer pool
數(shù)據(jù)庫在啟動階段是如何初始化buffer pool的呢?
當數(shù)據(jù)庫啟動的時候便會向操作系統(tǒng)申請一塊內(nèi)存給buffer pool,內(nèi)存區(qū)域申請完畢后,就會按照默認的緩存頁大小16kb和描述信息塊的800左右的字節(jié)在buffer pool中劃分出來一個一個的緩存頁和他們所對應的描述數(shù)據(jù)塊。
只不過這時的緩存頁都是空的,等數(shù)據(jù)庫完全運行起來,我們執(zhí)行增刪查改的時候,才會把對應的數(shù)據(jù)頁加載到緩存頁中。
5. 如何區(qū)分空閑緩存頁--free鏈表
在不停的增刪改查下,MySQL會不停的把數(shù)據(jù)頁從磁盤中加載到緩存中里。隨著16kb的數(shù)據(jù)頁不停的加載,那是如何判斷哪些緩存頁是空閑的?
MySQL為buffer pool設計了free鏈表。在buffer pool中,每一個空閑的緩存頁對應的描述數(shù)據(jù)塊通過頭節(jié)點和尾節(jié)點連接在一起,形成一個free鏈表。
每當數(shù)據(jù)頁加載到buffer pool中,對應描述數(shù)據(jù)塊就會記錄相關的信息,找到相應的緩存頁緩存數(shù)據(jù)頁,然后從free鏈表中移除此描述數(shù)據(jù)塊。
即如果緩存頁是空閑的,他的描述數(shù)據(jù)塊必定存在這個雙向鏈表中。

除此之外,還會有一個基礎節(jié)點,引用鏈表的頭節(jié)點和尾節(jié)點,記錄free鏈表的節(jié)點數(shù),即空閑緩存頁的個數(shù)。
free鏈表是由buffer pool的空閑描述數(shù)據(jù)塊組成,并不會占用額外的內(nèi)存空間,如果緩存頁加載了數(shù)據(jù),只會把節(jié)點從鏈表中移除。
6. 把磁盤的數(shù)據(jù)頁加載到buffer pool的緩存頁
有了free鏈表,這一步就很簡單了。
首先從free鏈表中獲取一個描述數(shù)據(jù)塊,就可以獲取到對應的空閑緩存頁,寫入描述信息,把磁盤上的數(shù)據(jù)頁讀取到緩存頁,最后移除對應的描述數(shù)據(jù)塊。

關于數(shù)據(jù)塊如何從free鏈表中移除,可以參考以下偽代碼:
假設節(jié)點02,上一個節(jié)點是01,下一個節(jié)點是03
DescriprionDataBlock() {
block_id = 02;
free_pre = 01;
free_next = 03;
}
當03節(jié)點從free鏈表中移除,則把free_next 設置為null就可以了,03在free鏈表中就是去了引用關系。
DescriprionDataBlock() {
block_id = 02;
free_pre = 01;
free_next = null;
}
7. 如何判斷數(shù)據(jù)頁有沒有被緩存?
在執(zhí)行增刪查改的時候,首先肯定是判斷數(shù)據(jù)頁有沒有被緩存,如果沒有,則走上面的邏輯,從free鏈表中找一個空閑的緩存頁,從磁盤讀取數(shù)據(jù)頁寫入緩存頁,寫入描述信息,從free鏈表中移除此描述數(shù)據(jù)塊。
如果數(shù)據(jù)頁已經(jīng)存在緩存里,那么就會直接使用。
所以MySQL還會有一個哈希表數(shù)據(jù)結構,用表空間號+數(shù)據(jù)頁編號作為kye,緩存頁地址作為value。
當需要數(shù)據(jù)頁的時,就會用表空間號+數(shù)據(jù)頁編號作為key去哈希表查,如果沒有就讀取數(shù)據(jù)頁,如果有就說明數(shù)據(jù)頁已經(jīng)被緩存了。
也就是說,每將數(shù)據(jù)頁讀取到緩存頁,都會在哈希表寫入一個key-value。

8. 臟頁--flush鏈表
在buffer pool的緩存頁中,有查詢出來的還沒有更新的,有已經(jīng)更新過數(shù)據(jù)的。更新過數(shù)據(jù)的緩存頁肯定和磁盤上的不一致,這些緩存頁便是臟數(shù)據(jù),臟頁。
這些臟頁最終都是被隨機刷入磁盤,但如何區(qū)分哪些緩存頁是臟頁?
數(shù)據(jù)庫引入了跟free鏈表相似的flush鏈表,本質(zhì)上flush鏈表也是有緩存頁的描述數(shù)據(jù)塊的兩個指針組成的一個鏈表,只是這里是被修改過的緩存頁的描述數(shù)據(jù)塊。
但凡修改過的緩存頁,都會把它相應的描述數(shù)據(jù)塊加入到flush鏈表中,等待刷盤。
