什么是數(shù)據(jù)庫的 “緩存池” ?(萬字干貨)
1、Buffer Pool 概述
Buffer Pool 是什么?從字面上看是緩存池的意思,沒錯,它其實也就是緩存池的意思。它是 MySQL 當中至關(guān)重要的一個組件,可以這么說,MySQL的所有的增刪改的操作都是在 Buffer Pool 中執(zhí)行的。
但是數(shù)據(jù)不是在磁盤中的嗎?怎么會和緩存池又有什么關(guān)系呢?那是因為如果 MySQL的操作都在磁盤中進行,那很顯然效率是很低的,效率為什么低?因為數(shù)據(jù)庫要從磁盤中拿數(shù)據(jù)啊,那肯定就需要IO啊,并且數(shù)據(jù)庫并不知道它將要查找的數(shù)據(jù)是磁盤的哪個位置,所以這就需要進行隨機IO,那這個性能簡直就別玩了。所以 MySQL對數(shù)據(jù)的操作都是在內(nèi)存中進行的,也就是在 Buffer Pool 這個內(nèi)存組件中。
實際上他就好比是 Redis,因為 Redis 是一個內(nèi)存是數(shù)據(jù)庫,他的操作就都是在內(nèi)存中進行的,并且會有一定的策略將其持久化到磁盤中。那 Buffer Pool 的內(nèi)存結(jié)構(gòu)具體是什么樣子的,那么多的增刪改操作難道數(shù)據(jù)要一直在內(nèi)存中嗎?既然說類似 redis 緩存,那是不是也像 redis 一樣也有一定的淘汰策略呢?
本篇文章,會詳細的介紹 Buffer Pool 的內(nèi)存結(jié)構(gòu),讓大家徹底明白這里面的每一步執(zhí)行流程。我們先看一下 MySQL從加載磁盤文件到完成提交一個事務(wù)的整個流程。我們先來看一個總體的流程圖,從數(shù)據(jù)在磁盤中被加載到緩存池中,然后經(jīng)過一些列的操作最終又被刷入到磁盤的一個過程,都經(jīng)歷了哪些事情,這個圖不明白沒有關(guān)系,因為本文重點是 Buffer Pool 這個整體的流程就是讓大家稍微有個印象。

2、Buffer Pool 有多大
Buffer Pool 是 InnoDB 中的一塊內(nèi)存區(qū)域,他一定是有自己的大小的,且大小默認是 128M,不過這個容量似乎有點小了,大家的自己的生產(chǎn)環(huán)境可以根據(jù)實際的內(nèi)存大小進行調(diào)整,參數(shù)為:innodb_buffer_pool_size=2147483648 單位是字節(jié),
# 查看和調(diào)整innodb_buffer_pool_size
1. 查看@@innodb_buffer_pool_size大小,單位字節(jié)
SELECT @@innodb_buffer_pool_size/1024/1024/1024; #字節(jié)轉(zhuǎn)為G
2. 在線調(diào)整InnoDB緩沖池大小,如果不設(shè)置,默認為128M
set global innodb_buffer_pool_size = 4227858432; ##單位字節(jié)
他在 InnoDB 中的整體結(jié)構(gòu)大概是這樣子的

3、數(shù)據(jù)頁
剛剛介紹到 MySQL在執(zhí)行增刪改的時候數(shù)據(jù)是會被加載到 Buffer Pool 中的,既然這樣數(shù)據(jù)是怎么被加載進來的,是一條一條還是說是以其他的形式呢。我們操作的數(shù)據(jù)都是以表 + 行的方式,而表 + 行僅僅是邏輯上的概念,MySQL并不會像我們一樣去操作行數(shù)據(jù),而是抽象出來一個一個的數(shù)據(jù)頁概念,每個數(shù)據(jù)頁的大小默認是 16KB,這些參數(shù)都是可以調(diào)整的。但是建議使用默認的就好,畢竟 MySQL能做到極致的都已經(jīng)做了。每個數(shù)據(jù)頁存放著多條的數(shù)據(jù),MySQL在執(zhí)行增刪改首先會定位到這條數(shù)據(jù)所在數(shù)據(jù)頁,然后會將數(shù)據(jù)所在的數(shù)據(jù)頁加載到 Buffer Pool 中。

4、緩存頁
當數(shù)據(jù)頁被加載到緩沖池中后,Buffer Pool 中也有叫緩存頁的概念與其一一對應(yīng),大小同樣是 16KB,但是 MySQL還為每個緩存也開辟額外的一些空間,用來描述對應(yīng)的緩存頁的一些信息,例如:數(shù)據(jù)頁所屬的表空間,數(shù)據(jù)頁號,這些描述數(shù)據(jù)塊的大小大概是緩存頁的15%左右(約800B)。
# 緩存頁是什么時候被創(chuàng)建的?
當 MSql 啟動的時候,就會初始化 Buffer Pool,這個時候 MySQL 會根據(jù)系統(tǒng)中設(shè)置的 innodb_buffer_pool_size 大小去內(nèi)存中申請一塊連續(xù)的內(nèi)存空間,實際上在這個內(nèi)存區(qū)域比配置的值稍微大一些,因為【描述數(shù)據(jù)】也是占用一定的內(nèi)存空間的,當在內(nèi)存區(qū)域申請完畢之后, MySql 會根據(jù)默認的緩存頁的大小(16KB)和對應(yīng)`緩存頁*15%`大小(800B左右)的數(shù)據(jù)描述的大小,將內(nèi)存區(qū)域劃分為一個個的緩存頁和對應(yīng)的描述數(shù)據(jù)

5、Free鏈表
上面是說了每個數(shù)據(jù)頁會被加載到一個緩存頁中,但是加載的時候 MySQL是如何知道那個緩存頁有數(shù)據(jù),那個緩存頁沒有數(shù)據(jù)呢?換句話說, MySQL是怎么區(qū)分哪些緩存頁是空閑的狀態(tài),是可以用來存放數(shù)據(jù)頁的。
為了解決這個問題, MySQL 為 Buffer Pool 設(shè)計了一個雙向鏈表— free鏈表,這個 free 鏈表的作用就是用來保存空閑緩存頁的描述塊(這句話這么說其實不嚴謹,換句話:每個空閑緩存頁的描述數(shù)據(jù)組成一個雙向鏈表,這個鏈表就是free鏈表)。之所以說free鏈表的作用就是用來保存空閑緩存頁的描述數(shù)據(jù)是為了先讓大家明白 free 鏈表的作用,另外 free 鏈表還會有一個基礎(chǔ)節(jié)點,他會引用該鏈表的頭結(jié)點和尾結(jié)點,還會記錄節(jié)點的個數(shù)(也就是可用的空閑的緩存頁的個數(shù))。
這個時候,他可以用下面的圖片來描述:

當加載數(shù)據(jù)頁到緩存池中的時候, MySQL會從 free 鏈表中獲取一個描述數(shù)據(jù)的信息,根據(jù)描述節(jié)點的信息拿到其對應(yīng)的緩存頁,然后將數(shù)據(jù)頁信息放到該緩存頁中,同時將鏈表中的該描述數(shù)據(jù)的節(jié)點移除。這就是數(shù)據(jù)頁被讀取 Buffer Pool 中的緩存頁的過程。
但 MySQL是怎么知道哪些數(shù)據(jù)頁已經(jīng)被緩存了,哪些沒有被緩存呢。實際上數(shù)據(jù)庫中還有后一個哈希表結(jié)構(gòu),他的作用是用來存儲表空間號 + 數(shù)據(jù)頁號作為數(shù)據(jù)頁的key,緩存頁對應(yīng)的地址作為其value,這樣數(shù)據(jù)在加載的時候就會通過哈希表中的key來確定數(shù)據(jù)頁是否被緩存了。

6、Flush鏈表
MySql 在執(zhí)行增刪改的時候會一直將數(shù)據(jù)以數(shù)據(jù)頁的形式加載到 Buffer Pool 的緩存頁中,增刪改的操作都是在內(nèi)存中執(zhí)行的,然后會有一個后臺的線程數(shù)將臟數(shù)據(jù)刷新到磁盤中,但是后臺的線程肯定是需要知道應(yīng)該刷新哪些啊。
針對這個問題,MySQL設(shè)計出了 Flush 鏈表,他的作用就是記錄被修改過的臟數(shù)據(jù)所在的緩存頁對應(yīng)的描述數(shù)據(jù)。如果內(nèi)存中的數(shù)據(jù)和數(shù)據(jù)庫和數(shù)據(jù)庫中的數(shù)據(jù)不一樣,那這些數(shù)據(jù)我們就稱之為臟數(shù)據(jù),臟數(shù)據(jù)之所以叫臟數(shù)據(jù),本質(zhì)上就是被緩存到緩存池中的數(shù)據(jù)被修改了,但是還沒有刷新到磁盤中。
同樣的這些已經(jīng)被修改了的數(shù)據(jù)所在的緩存頁的描述數(shù)據(jù)會被維護到 Flush 中(其實結(jié)構(gòu)和 free 鏈表是一樣的),所以 Flush 中維護的是一些臟數(shù)據(jù)數(shù)據(jù)描述(準確地說是臟數(shù)據(jù)的所在的緩存頁的數(shù)據(jù)描述)
另外,當某個臟緩存頁被刷新到磁盤后,其空間就騰出來了,然后又會跑到 Free 鏈表中了。

7、LRU鏈表
如果系統(tǒng)一直在進行數(shù)據(jù)庫的增刪改操作,數(shù)據(jù)庫內(nèi)部的基本流程就是:

我們還拿 redis 類做類比,以便更好的幫助大家明白其原理。Flush 的作用其實類似 redis 的 key 設(shè)置的過期時間,所以一般情況下,redis 內(nèi)存不會不夠使用,但是總有特殊的情況,問題往往就是在這種極端和邊邊角角的情況下產(chǎn)生的。
如果 redis 的內(nèi)存不夠使用了,是不是自己還有一定的淘汰策略?最基本的準則就是淘汰掉不經(jīng)常使用到的key。Buffer Pool 也類似,它也會有內(nèi)存不夠使用的情況,它是通過 LRU 鏈表來維護的。LRU 即 Least Recently Uesd(最近最少使用)。
MySql 會把最近使用最少的緩存頁數(shù)據(jù)刷入到磁盤去,那 MySql 如何判斷出 LRU 數(shù)據(jù)的呢?為此 MySql 專門設(shè)計了 LUR 鏈表,還引入了另一個概念:緩存命中率
# 緩存命中率
可以理解為緩存被使用到的頻率,舉個例子來說:現(xiàn)在有兩個緩存頁,在100次請求中A緩存頁被命中了20次,B緩存頁被命中了2次,很顯然A緩存頁的命中率更高,這也就意味著A在未來還會被使用到的可能性比較大,而B就會被 MySQL 認為基本不會被使用到;
說到這里,那LRU究竟是怎么工作的。假設(shè) MySQL在將數(shù)據(jù)加載到緩存池的時候,他會將被加載進來的緩存頁按照被加載進來的順序插入到LRU鏈表的頭部(就是鏈表的頭插法),假設(shè) MySQL現(xiàn)在先后分別加載A、B、C數(shù)據(jù)頁到緩存頁A、B、C中,然后 LRU 的鏈表大致是這樣子的。

現(xiàn)在又來了一個請求,假設(shè)查詢到的數(shù)據(jù)是已經(jīng)被緩存在緩存頁B中,這時候 MySQL就會將B緩存頁對應(yīng)的描述信息插入到LRU鏈表的頭部,如下圖:

然后又來了一個請求,數(shù)據(jù)是已經(jīng)被緩存在了緩存頁C中,然后LRU會變成這樣子:

說到底,每次查詢數(shù)據(jù)的時候如果數(shù)據(jù)已經(jīng)在緩存頁中,那么就會將該緩存頁對應(yīng)的描述信息放到LRU鏈表的頭部,如果不在緩存頁中,就去磁盤中查找,如果查找到了,就將其加載到緩存中,并將該數(shù)據(jù)對應(yīng)的緩存頁的描述信息插入到LRU鏈表的頭部。也就是說最近使用的緩存頁都會排在前面,而排在后面的說明是不經(jīng)常被使用到的。
最后,如果 Buffer Pool 不夠使用了,那么 MySQL就會將 LRU 鏈表中的尾節(jié)點刷入到磁盤中,用來給 Buffer Pool 騰出內(nèi)存空間。來個整體的流程圖給大家看下

8、LRU鏈表帶來的麻煩
這里的麻煩指的是就是 MySQL本身的預(yù)讀機制帶來的問題
# 預(yù)讀機制
MySQL 在從磁盤加載數(shù)據(jù)的的時候,會將數(shù)據(jù)頁的相鄰的其他的數(shù)據(jù)頁也加載到緩存中。
# MySQL 為什么要這么做
因為根據(jù)經(jīng)驗和習(xí)慣,一般查詢數(shù)據(jù)的時候往往還會查詢該數(shù)據(jù)相鄰前后的一些數(shù)據(jù),有人可能會反問:一個數(shù)據(jù)頁上面不是就會存在該條數(shù)據(jù)相鄰的數(shù)據(jù)嗎?這可不一定,某條數(shù)據(jù)可能很大,也可能這條數(shù)據(jù)是在數(shù)據(jù)頁在頭部,也可能是在數(shù)據(jù)頁的尾部,所以 MySQL 為了提高效率,會將某個數(shù)據(jù)頁的相鄰的數(shù)據(jù)頁也加載到緩存池中。

上圖能夠看到B的相鄰也被加載到了C描述數(shù)據(jù)的前面,而實際上C的命中率比B的相鄰頁高多了,這就是LRU本身帶來的問題。
# 哪些情況會觸發(fā)預(yù)讀機制
1. 有一個參數(shù)是 innodb_read_ahead_threshold, 他的默認值是56,意思就是如果順序的訪問了一個區(qū)里的多個數(shù)據(jù)頁,訪問的數(shù)據(jù)頁的數(shù)量超過了這個閾值,此時就會觸發(fā)預(yù)讀機制,把下一個相鄰區(qū)中的所有數(shù)據(jù)頁都加載到緩存里去(這種就是:線性預(yù)讀)
2. 如果 Buffer Pool 里緩存了一個區(qū)里的13個連續(xù)的數(shù)據(jù)頁,而且這些數(shù)據(jù)頁都是比較頻繁會被訪問的,此時就會直接觸發(fā)預(yù)讀機制,把這個區(qū)里的其他的數(shù)據(jù)頁都加載到緩存里去(這種就是:隨機預(yù)讀)隨機預(yù)讀是通過:innodb_random_read_ahead 來控制的,默認是OFF即關(guān)閉的(MySQL 5.5已經(jīng)基本飛起該功能,應(yīng)為他會帶來不必要的麻煩,這里也不推薦大家開啟,說出來的目的是讓大家了解下有這么個東西)
還有一種情況是 SELECT * FROM students 這種直接全表掃描的,會直接加載表中的所有的數(shù)據(jù)到緩存中,這些數(shù)據(jù)基本是加載的時候查詢一次,后面就基本使用不到了,但是加載這么多數(shù)據(jù)到鏈表的頭部就將其他的經(jīng)常命中的緩存頁直接全擠到后面去了。
以上種種跡象表明,預(yù)讀機制帶來的問題還是蠻大的,既然這么大,那 MySQL為什么還要進入預(yù)讀機制呢,說到底還是為了提高效率,**一種新的技術(shù)的引進,往往帶來新的挑戰(zhàn)**,下面我們就一起來看下 MySQL是如何解決預(yù)加載所帶來的麻煩的。
9、基于冷熱數(shù)據(jù)分離的LRU鏈表
所謂的冷熱分離,就是將 LRU 鏈表分成兩部分,一部分是經(jīng)常被使用到的熱數(shù)據(jù),另一部分是被加載進來但是很少使用的冷數(shù)據(jù)。通過參數(shù)innodb_old_blocks_pct 參數(shù)控制的,默認為37,也就是 37% 。用圖表示大致如下:

數(shù)據(jù)在從磁盤被加載到緩存池的時候,首先是會被放在冷數(shù)據(jù)區(qū)的頭部,然后在一定時間之后,如果再次訪問了這個數(shù)據(jù),那么這個數(shù)據(jù)所在的緩存頁對應(yīng)描述數(shù)據(jù)就會被放轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部。
那為什么說是在一定的時間之后呢,假設(shè)某條數(shù)據(jù)剛被加載到緩存池中,然后緊接著又被訪問了一次,這個時候假設(shè)就將其轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部,但是以后就再也不會被使用了,這樣子是不是就還是會存在之前的問題呢?
所以 MySQL通過innodb_old_blocks_time來設(shè)置數(shù)據(jù)被加載到緩存池后的多少時間之后再次被訪問,才會將該數(shù)據(jù)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部,該參數(shù)默認是1000單位為:毫秒,也就是1秒之后,如果該數(shù)據(jù)又被訪問了,那么這個時候才會將該數(shù)據(jù)從 LRU 鏈表的冷數(shù)據(jù)區(qū)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)。
現(xiàn)在再回頭看下上面的問題
# 通過預(yù)加載(加載相鄰數(shù)據(jù)頁)進來的數(shù)據(jù)
1. 這個時候就很好理解了,反正數(shù)據(jù)會被放在LRU鏈表的冷數(shù)據(jù)區(qū)的(注意:這里說的放在鏈表中的數(shù)據(jù)都是指的是<緩存頁中的數(shù)據(jù)所對應(yīng)的描述數(shù)據(jù)>),當在指定時候之后,如果某些緩存頁被訪問了那么就將該緩存頁的描述數(shù)據(jù)放到熱數(shù)據(jù)區(qū)鏈表的頭部
# 全表掃描加載進來的數(shù)據(jù)頁
1. 和上面一樣,數(shù)據(jù)都是先在冷數(shù)據(jù)區(qū),然后在一定時間之后,再次被訪問到的數(shù)據(jù)頁才會轉(zhuǎn)移到熱數(shù)據(jù)區(qū)的鏈表的頭結(jié)點,所以這也就很好的解決了全表掃描所帶來的問題
再來思考下 Buffer Pool 內(nèi)存不夠的問題
# Buffer Pool 內(nèi)存空間不夠使用了怎么辦?也就是說沒有足夠使用的空閑的緩存頁了。
1. 這個問題在這個時候就顯得非常簡單了,直接將鏈表冷數(shù)據(jù)區(qū)的尾節(jié)點的描述數(shù)據(jù)多對應(yīng)的緩存頁刷到磁盤即可。
但是這樣子還不是足夠完美,為什么這么說,剛剛我們一直在討論的是冷數(shù)據(jù)區(qū)的數(shù)據(jù)被訪問,然后在一定規(guī)則之下會被加載到熱數(shù)據(jù)鏈表的頭部,但是現(xiàn)在某個請求需要訪問的數(shù)據(jù)就在熱數(shù)據(jù)區(qū),那是不是直接把該數(shù)據(jù)所在的緩存頁對應(yīng)的描述數(shù)據(jù)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表頭部呢?
很顯然不是這樣子的,因為熱數(shù)據(jù)區(qū)的數(shù)據(jù)本身就是會被頻繁訪問的,這樣子如果每次訪問都去移動鏈表,勢必造成性能的下降(影響再小極端情況下也可能會不可控),所以 MySQL針對熱數(shù)據(jù)區(qū)的數(shù)據(jù)的轉(zhuǎn)移也有相關(guān)的規(guī)則。
該規(guī)則就是:如果被訪問的數(shù)據(jù)所在的緩存頁在熱數(shù)據(jù)區(qū)的前25%,那么該緩存頁對應(yīng)的描述數(shù)據(jù)是不會被轉(zhuǎn)移到熱數(shù)據(jù)鏈表的頭部的,只有當被訪問的緩存頁對應(yīng)的描述數(shù)據(jù)在熱數(shù)據(jù)區(qū)鏈表的后75%,該緩存頁的描述數(shù)據(jù)才會被轉(zhuǎn)移到熱數(shù)據(jù)鏈表的頭部。
舉個例子來說,假設(shè)熱數(shù)據(jù)區(qū)有100個緩存頁(這里的緩存頁還是指的是緩存頁對應(yīng)的描述數(shù)據(jù),再強調(diào)下,鏈表中存放的是緩存頁的描述數(shù)據(jù),為了方便有時候會直接說緩存頁。希望朋友們注意),當被訪問的緩存頁在前25個的時候,熱數(shù)據(jù)區(qū)的鏈表是不會有變化的,當被訪問的緩存頁在26~100(也就是數(shù)據(jù)在熱數(shù)據(jù)區(qū)鏈表的后75%里面)的時候,這個時候被訪問的緩存頁才會被轉(zhuǎn)移到鏈表的頭部。
到此為止, MySQL對于LUR 鏈表的優(yōu)化就堪稱完美了。是不是看到這里瞬間感覺很多東西都明朗了,好了,對于 LRU 鏈表我們就討論到這里了。
10、Buffer Pool 中的鏈表小結(jié)
# free鏈表
用來存放空閑的緩存頁的描述數(shù)據(jù),如果某個緩存頁被使用了,那么該緩存頁對應(yīng)的描述數(shù)據(jù)就會被從free鏈表中移除
# flush鏈表
被修改的臟數(shù)據(jù)都記錄在 Flush 中,同時會有一個后臺線程會不定時的將 Flush 中記錄的描述數(shù)據(jù)對應(yīng)的緩存頁刷新到磁盤中,如果某個緩存頁被刷新到磁盤中了,那么該緩存頁對應(yīng)的描述數(shù)據(jù)會從 Flush 中移除,同時也會從LRU鏈表中移除(因為該數(shù)據(jù)已經(jīng)不在 Buffer Pool 中了,已經(jīng)被刷入到磁盤,所以就也沒必要記錄在 LRU 鏈表中了),同時還會將該緩存頁的描述數(shù)據(jù)添加到free鏈表中,因為該緩存頁變得空閑了。
# LRU鏈表
數(shù)據(jù)頁被加載到 Buffer Pool 中的對應(yīng)的緩存頁后,同時會將緩存頁對應(yīng)的描述數(shù)據(jù)放到 LRU 鏈表的冷數(shù)據(jù)的頭部,當在一定時間過后,冷數(shù)據(jù)區(qū)的數(shù)據(jù)被再次訪問了,就會將其轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部,如果被訪問的數(shù)據(jù)就在熱數(shù)據(jù)區(qū),那么如果是在前25%就不會移動,如果在后75%仍然會將其轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部
后臺線程將冷數(shù)據(jù)區(qū)的尾節(jié)點的描述數(shù)據(jù)對應(yīng)的緩存頁刷入磁盤文件中

11、Buffer Pool 的并發(fā)性能
我們平時的系統(tǒng)絕對不可能每次只有一個請求來訪問的,說白了就是如果多個請求同時來執(zhí)行增刪改,那他們會并行的去操作 Buffer Pool 中的各種鏈表嗎?如果是并行的會不會有什么問題。
實際上 MySQL在處理這個問題的時候考慮的非常簡單,就是: Buffer Pool 一次只能允許一個線程來操作,一次只有一個線程來執(zhí)行這一系列的操作,因為MySQL 為了保證數(shù)據(jù)的一致性,操作的時候必須緩存池加鎖,一次只能有一個線程獲取到鎖。
這個時候,大家這時候肯定滿腦子問號。串行那還談什么效率?大家別忘記了,這一系列的操作都是在內(nèi)存中操作的,實際上這是一個瞬時的過程,在內(nèi)存中的操作基本是幾毫秒的甚至微妙級別的事情。
但是話又說回來,串行執(zhí)行再怎么快也是串行,雖然不是性能瓶頸,這還有更好的優(yōu)化辦法嗎?那肯定的 MySQL早就設(shè)計好了這些規(guī)則。那就是 Buffer Pool 是可以有多個的,可以通過 MySQL的配置文件來配置,參數(shù)分別是:
# Buffer Pool 的總大小
innodb_buffer_pool_size=8589934592
# Buffer Pool 的實例數(shù)(個數(shù))
innodb_buffer_pool_instance=4
一般在生產(chǎn)環(huán)境中,在硬件不緊張的情況下,建議使用此策略。這個時候大家是不是又會有一個疑問(如果沒有那說明你沒認真思考哦),大家應(yīng)該有這樣的疑問:
# 問:多個 Buffer Pool 所帶來的問題思考
在多個線程訪問不同的 Buffer Pool 那不同的線程加載的數(shù)據(jù)必然是在不同的 Buffer Pool 中,假設(shè) A 線程加載數(shù)據(jù)頁A到 Buffer Pool A 中,B 線程加載數(shù)據(jù)頁B到 Buffer Pool B 中,然后兩個都執(zhí)行完了,這個時候 C 線程來了,他到達的是 Buffer Pool B中,但是 C 要訪問的數(shù)據(jù)是在 Buffer Pool A中的數(shù)據(jù)頁上了,這個時候 C 還會去加載數(shù)據(jù)頁A嗎?,這種情況會發(fā)生嗎?在不同的 Buffer Pool 緩存中會去緩存相同的數(shù)據(jù)頁嗎?
# 答:多個 Buffer Pool 所帶來的問題解答
這種情況很顯然不會發(fā)生,既然不會發(fā)生,那 MySql 是如何解決這種問題的?其實前面已經(jīng)提到過了,那就是 數(shù)據(jù)頁緩存哈希表(看下圖),里面存放的是表空間號+數(shù)據(jù)頁號 = 緩存頁地址,所以 MySQL 在加載數(shù)據(jù)所在的數(shù)據(jù)頁的時候根據(jù)這一系列的映射關(guān)系判斷數(shù)據(jù)頁是否被加載,被加載到了那個緩存頁中,所以 MySQL 能夠精確的確定某個數(shù)據(jù)頁是否被加載,被加載的到了哪個緩存頁,絕不可能出現(xiàn)重復(fù)加載的情況。

12、動態(tài)調(diào)整 Buffer Pool 的大小
到此為止,本文已經(jīng)詳細的介紹了 Buffer Pool 的內(nèi)存結(jié)構(gòu),它的數(shù)據(jù)是如何存放的,如何刷磁盤的,又是如何加載的,以什么樣的形式存在的等等知識點,下面我們繼續(xù)挖掘,將 Buffer Pool 的相關(guān)知識點一次說個夠。我們現(xiàn)在來討論下 Buffer Pool 的大小能否動態(tài)調(diào)整。
假設(shè)我們現(xiàn)在的 Buffer Pool 的大小是 2GB大小,現(xiàn)在想將其擴大到 4GB,現(xiàn)在說一下如果真的要這么做,我們的 MySq 需要做哪些事情。首先 ,MySQL 需要向操作系統(tǒng)申請一塊大小為 4G 的連續(xù)的地址連續(xù)的內(nèi)存空間,然后將原來的 Buffer Pool 中的數(shù)據(jù)拷貝到新的 Buffer Pool 中。
這樣可能嗎?如果原來的是8G,擴大到 16G,那這個將原來的數(shù)據(jù)復(fù)制到新的 Buffer Pool 中是不是極為耗時的,所以這樣的操作 MySQL必然是不支持的。但實際上這樣的需求是客觀存在的,那 MySQL是如何解決的呢?
為了處理這種情況,MySQL設(shè)計出 chunk (http 協(xié)議中也有使用到這個思想,所以我們會發(fā)現(xiàn)很多技術(shù)的優(yōu)秀思想都是在相互借鑒)機制來解決的
# 什么是chunk機制
chunk是 MySQL 設(shè)計的一種機制,這種機制的原理是將 Buffer Pool 拆分一個一個大小相等的 chunk 塊,每個 chunk 默認大小為 128M(可以通過參數(shù)innodb_buffer_pool_chunk_size 來調(diào)整大小),也就是說 Buffer Pool 是由一個個的chunk組成的
假設(shè) Buffer Pool 大小是2GB,而一個chunk大小默認是128M,也就是說一個2GB大小的 Buffer Pool 里面由16個 chunk 組成,每個chunk中有自己的緩存頁和描述數(shù)據(jù),而 free 鏈表、flush 鏈表和 lru 鏈表是共享的

如果說有多個 Buffer Pool ,那就是這樣

說到這里好像還是沒有說到 MySQL到底是如何通過 chunk 機制來調(diào)整大小的。實際上是這樣的,假設(shè)現(xiàn)在 Buffer Pool 有 2GB,里面有16個chunk,現(xiàn)在想要擴大到 4GB,那么這個時候只需要新申請一個個的 chunk 就可以了。
這樣不但不需要申請一塊很大的連續(xù)的空間,更不需要將復(fù)制數(shù)據(jù)。這樣就能達到動態(tài)調(diào)整大小了(不會還有人問:這只是擴大,怎么縮小呢?gun)。不得不說 MySQL真機智。
13、生產(chǎn)環(huán)境如何設(shè)置 Buffer Pool 大小
Buffer Pool 是不是越大越好,理論上是的。那如果一個機器內(nèi)存是16GB那分配給 Buffer Pool 15GB,這樣很顯然是不行的,因為操作系統(tǒng)要占內(nèi)存,你的機器上總會運行其他的進行的吧?那肯定也是需要占用內(nèi)存的。根據(jù)很多實際生產(chǎn)經(jīng)驗得出的比較合理的大小是機器內(nèi)存大小的(50%~60%)。
最后一起來看看你的 INNODB 的相關(guān)參數(shù),命令是show engine innodb status
show engine innodb status;
----------------------
Buffer Pool AND MEMORY
----------------------
-- Buffer Pool 的最終大小
Total memory allocated
-- Buffer Pool 一共有多少個緩存頁
Buffer Pool size
-- free 鏈表中一共有多少個緩存也是可以使用的
Free buffers
-- lru鏈表中一共有多少個緩存頁
Database pages
-- lru鏈表鏈表中的冷數(shù)據(jù)區(qū)一共有多少個緩存頁
Old database pages
-- flush鏈表中的緩存頁的數(shù)量
Modified db pages
-- 等待從磁盤上加載進來的緩存頁的數(shù)量
Pending reads
-- 即將從lru鏈表中刷入磁盤的數(shù)量,flush鏈表中即將刷入磁盤的緩存頁的數(shù)量
Pending writes: LRU 0, flush list 0, single page 0
-- lru鏈表的冷數(shù)據(jù)區(qū)的緩存頁被訪問之后轉(zhuǎn)移到熱數(shù)據(jù)區(qū)的緩存頁的數(shù)量,以及冷數(shù)據(jù)區(qū)里1s之內(nèi)被訪問但是沒有進入到熱數(shù)據(jù)區(qū)的緩存頁的數(shù)量
Pages made young 260368814, not young 0
-- 每秒從冷數(shù)據(jù)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)的緩存頁的數(shù)量,以及每秒在冷數(shù)據(jù)區(qū)被訪問但是沒有進入熱數(shù)據(jù)區(qū)的緩存頁的數(shù)量
332.69 youngs/s, 0.00 non-youngs/s
-- 已經(jīng)讀取創(chuàng)建和寫入的緩存頁的數(shù)量,以及每秒讀取、創(chuàng)建和寫入的緩存頁的數(shù)量
Pages read 249280313, created 1075315, written 32924991 359.96 reads/s, 0.02 creates/s, 0.23 writes/s
-- 表示1000次訪問中,有多少次是命中了BufferPool緩存中的緩存頁,以及每1000次訪問有多少數(shù)據(jù)從冷數(shù)據(jù)區(qū)轉(zhuǎn)移到熱數(shù)據(jù)區(qū),以及沒有轉(zhuǎn)移的緩存頁的數(shù)量
Buffer Pool hit rate 867 / 1000, young-making rate 123 / 1000 not 0 / 1000
-- lru鏈表中緩存頁的數(shù)量
LRU len: 8190
-- 最近50s讀取磁盤頁的總數(shù),cur[0]表示現(xiàn)在正在讀取的磁盤頁的總數(shù)
I/O sum[5198]:cur[0],
14、結(jié)束語
本篇文章我們詳細討論了 Buffer Pool 的內(nèi)存結(jié)構(gòu),從 free 鏈表到 lru 鏈表,從 Buffer Pool 到 chunk,從磁盤中加載一個數(shù)據(jù)頁到 Buffer Pool 到最后該數(shù)據(jù)頁又被刷回到磁盤中的一整個過程,他的每一步都做了什么。
我們一起討論完本文以后,是不是瞬間有種看透來了 MySQL的感覺,但是這個僅僅是前提,學(xué)習(xí)這些的目的是為了更好的理解 MySQL讓我們能夠在工作中更加游刃有余地使用它。因為只有在知道了底層原理的情況下,才能熟悉他的工作原理,遇到問題才能對癥下藥。
—————END—————
喜歡本文的朋友,歡迎關(guān)注公眾號 程序員小灰,收看更多精彩內(nèi)容
點個[在看],是對小灰最大的支持!

