Buffer Pool 概述
Buffer Pool 是什么?從字面上看是緩存池的意思,沒錯(cuò),它其實(shí)也就是緩存池的意思。它是 MySQL 當(dāng)中至關(guān)重要的一個(gè)組件,可以這么說,MySQL的所有的增刪改的操作都是在 Buffer Pool 中執(zhí)行的。
但是數(shù)據(jù)不是在磁盤中的嗎?怎么會(huì)和緩存池又有什么關(guān)系呢?那是因?yàn)槿绻?MySQL的操作都在磁盤中進(jìn)行,那很顯然效率是很低的,效率為什么低?因?yàn)閿?shù)據(jù)庫要從磁盤中拿數(shù)據(jù)啊,那肯定就需要IO啊,并且數(shù)據(jù)庫并不知道它將要查找的數(shù)據(jù)是磁盤的哪個(gè)位置,所以這就需要進(jìn)行隨機(jī)IO,那這個(gè)性能簡直就別玩了。所以 MySQL對(duì)數(shù)據(jù)的操作都是在內(nèi)存中進(jìn)行的,也就是在 Buffer Pool 這個(gè)內(nèi)存組件中。
實(shí)際上他就好比是 redis,因?yàn)?Redis 是一個(gè)內(nèi)存是數(shù)據(jù)庫,他的操作就都是在內(nèi)存中進(jìn)行的,并且會(huì)有一定的策略將其持久化到磁盤中。那 Buffer Pool 的內(nèi)存結(jié)構(gòu)具體是什么樣子的,那么多的增刪改操作難道數(shù)據(jù)要一直在內(nèi)存中嗎?既然說類似 redis 緩存,那是不是也像 redis 一樣也有一定的淘汰策略呢?
本篇文章,會(huì)詳細(xì)的介紹 Buffer Pool 的內(nèi)存結(jié)構(gòu),讓大家徹底明白這里面的每一步執(zhí)行流程。我們先看一下 MySQL從加載磁盤文件到完成提交一個(gè)事務(wù)的整個(gè)流程。我們先來看一個(gè)總體的流程圖,從數(shù)據(jù)在磁盤中被加載到緩存池中,然后經(jīng)過一些列的操作最終又被刷入到磁盤的一個(gè)過程,都經(jīng)歷了哪些事情,這個(gè)圖不明白沒有關(guān)系,因?yàn)楸疚闹攸c(diǎn)是 Buffer Pool 這個(gè)整體的流程就是讓大家稍微有個(gè)印象。
2、Buffer Pool 有多大
Buffer Pool 是 InnoDB 中的一塊內(nèi)存區(qū)域,他一定是有自己的大小的,且大小默認(rèn)是 128M,不過這個(gè)容量似乎有點(diǎn)小了,大家的自己的生產(chǎn)環(huán)境可以根據(jù)實(shí)際的內(nèi)存大小進(jìn)行調(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è)置,默認(rèn)為128M
set global innodb_buffer_pool_size = 4227858432; ##單位字節(jié)
他在 InnoDB 中的整體結(jié)構(gòu)大概是這樣子的
3、數(shù)據(jù)頁
剛剛介紹到 MySQL在執(zhí)行增刪改的時(shí)候數(shù)據(jù)是會(huì)被加載到 Buffer Pool 中的,既然這樣數(shù)據(jù)是怎么被加載進(jìn)來的,是一條一條還是說是以其他的形式呢。我們操作的數(shù)據(jù)都是以表 + 行的方式,而表 + 行僅僅是邏輯上的概念,MySQL并不會(huì)像我們一樣去操作行數(shù)據(jù),而是抽象出來一個(gè)一個(gè)的數(shù)據(jù)頁概念,每個(gè)數(shù)據(jù)頁的大小默認(rèn)是 16KB,這些參數(shù)都是可以調(diào)整的。但是建議使用默認(rèn)的就好,畢竟 MySQL能做到極致的都已經(jīng)做了。每個(gè)數(shù)據(jù)頁存放著多條的數(shù)據(jù),MySQL在執(zhí)行增刪改首先會(huì)定位到這條數(shù)據(jù)所在數(shù)據(jù)頁,然后會(huì)將數(shù)據(jù)所在的數(shù)據(jù)頁加載到 Buffer Pool 中。
4、緩存頁
當(dāng)數(shù)據(jù)頁被加載到緩沖池中后,Buffer Pool 中也有叫緩存頁的概念與其一一對(duì)應(yīng),大小同樣是 16KB,但是 MySQL還為每個(gè)緩存也開辟額外的一些空間,用來描述對(duì)應(yīng)的緩存頁的一些信息,例如:數(shù)據(jù)頁所屬的表空間,數(shù)據(jù)頁號(hào),這些描述數(shù)據(jù)塊的大小大概是緩存頁的15%左右(約800B)。
# 緩存頁是什么時(shí)候被創(chuàng)建的?
當(dāng) MSql 啟動(dòng)的時(shí)候,就會(huì)初始化 Buffer Pool,這個(gè)時(shí)候 MySQL 會(huì)根據(jù)系統(tǒng)中設(shè)置的 innodb_buffer_pool_size 大小去內(nèi)存中申請(qǐng)一塊連續(xù)的內(nèi)存空間,實(shí)際上在這個(gè)內(nèi)存區(qū)域比配置的值稍微大一些,因?yàn)椤久枋鰯?shù)據(jù)】也是占用一定的內(nèi)存空間的,當(dāng)在內(nèi)存區(qū)域申請(qǐng)完畢之后, MySql 會(huì)根據(jù)默認(rèn)的緩存頁的大小(16KB)和對(duì)應(yīng)`緩存頁*15%`大小(800B左右)的數(shù)據(jù)描述的大小,將內(nèi)存區(qū)域劃分為一個(gè)個(gè)的緩存頁和對(duì)應(yīng)的描述數(shù)據(jù)
5、Free鏈表
上面是說了每個(gè)數(shù)據(jù)頁會(huì)被加載到一個(gè)緩存頁中,但是加載的時(shí)候 MySQL是如何知道那個(gè)緩存頁有數(shù)據(jù),那個(gè)緩存頁沒有數(shù)據(jù)呢?換句話說, MySQL是怎么區(qū)分哪些緩存頁是空閑的狀態(tài),是可以用來存放數(shù)據(jù)頁的。
為了解決這個(gè)問題, MySQL 為 Buffer Pool 設(shè)計(jì)了一個(gè)雙向鏈表— free鏈表,這個(gè) free 鏈表的作用就是用來保存空閑緩存頁的描述塊(這句話這么說其實(shí)不嚴(yán)謹(jǐn),換句話:每個(gè)空閑緩存頁的描述數(shù)據(jù)組成一個(gè)雙向鏈表,這個(gè)鏈表就是free鏈表)。之所以說free鏈表的作用就是用來保存空閑緩存頁的描述數(shù)據(jù)是為了先讓大家明白 free 鏈表的作用,另外 free 鏈表還會(huì)有一個(gè)基礎(chǔ)節(jié)點(diǎn),他會(huì)引用該鏈表的頭結(jié)點(diǎn)和尾結(jié)點(diǎn),還會(huì)記錄節(jié)點(diǎn)的個(gè)數(shù)(也就是可用的空閑的緩存頁的個(gè)數(shù))。
這個(gè)時(shí)候,他可以用下面的圖片來描述:
當(dāng)加載數(shù)據(jù)頁到緩存池中的時(shí)候, MySQL會(huì)從 free 鏈表中獲取一個(gè)描述數(shù)據(jù)的信息,根據(jù)描述節(jié)點(diǎn)的信息拿到其對(duì)應(yīng)的緩存頁,然后將數(shù)據(jù)頁信息放到該緩存頁中,同時(shí)將鏈表中的該描述數(shù)據(jù)的節(jié)點(diǎn)移除。這就是數(shù)據(jù)頁被讀取 Buffer Pool 中的緩存頁的過程。
但 MySQL是怎么知道哪些數(shù)據(jù)頁已經(jīng)被緩存了,哪些沒有被緩存呢。實(shí)際上數(shù)據(jù)庫中還有后一個(gè)哈希表結(jié)構(gòu),他的作用是用來存儲(chǔ)表空間號(hào) + 數(shù)據(jù)頁號(hào)作為數(shù)據(jù)頁的key,緩存頁對(duì)應(yīng)的地址作為其value,這樣數(shù)據(jù)在加載的時(shí)候就會(huì)通過哈希表中的key來確定數(shù)據(jù)頁是否被緩存了。
6、Flush鏈表
MySql 在執(zhí)行增刪改的時(shí)候會(huì)一直將數(shù)據(jù)以數(shù)據(jù)頁的形式加載到 Buffer Pool 的緩存頁中,增刪改的操作都是在內(nèi)存中執(zhí)行的,然后會(huì)有一個(gè)后臺(tái)的線程數(shù)將臟數(shù)據(jù)刷新到磁盤中,但是后臺(tái)的線程肯定是需要知道應(yīng)該刷新哪些啊。
針對(duì)這個(gè)問題,MySQL設(shè)計(jì)出了 Flush 鏈表,他的作用就是記錄被修改過的臟數(shù)據(jù)所在的緩存頁對(duì)應(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ù)會(huì)被維護(hù)到 Flush 中(其實(shí)結(jié)構(gòu)和 free 鏈表是一樣的),所以 Flush 中維護(hù)的是一些臟數(shù)據(jù)數(shù)據(jù)描述(準(zhǔn)確地說是臟數(shù)據(jù)的所在的緩存頁的數(shù)據(jù)描述)
另外,當(dāng)某個(gè)臟緩存頁被刷新到磁盤后,其空間就騰出來了,然后又會(huì)跑到 Free 鏈表中了。
7、LRU鏈表
如果系統(tǒng)一直在進(jìn)行數(shù)據(jù)庫的增刪改操作,數(shù)據(jù)庫內(nèi)部的基本流程就是:
我們還拿 redis 類做類比,以便更好的幫助大家明白其原理。Flush 的作用其實(shí)類似 redis 的 key 設(shè)置的過期時(shí)間,所以一般情況下,redis 內(nèi)存不會(huì)不夠使用,但是總有特殊的情況,問題往往就是在這種極端和邊邊角角的情況下產(chǎn)生的。
如果 redis 的內(nèi)存不夠使用了,是不是自己還有一定的淘汰策略?最基本的準(zhǔn)則就是淘汰掉不經(jīng)常使用到的key。Buffer Pool 也類似,它也會(huì)有內(nèi)存不夠使用的情況,它是通過 LRU 鏈表來維護(hù)的。LRU 即 Least Recently Uesd(最近最少使用)。
MySql 會(huì)把最近使用最少的緩存頁數(shù)據(jù)刷入到磁盤去,那 MySql 如何判斷出 LRU 數(shù)據(jù)的呢?為此 MySql 專門設(shè)計(jì)了 LUR 鏈表,還引入了另一個(gè)概念:緩存命中率
# 緩存命中率
可以理解為緩存被使用到的頻率,舉個(gè)例子來說:現(xiàn)在有兩個(gè)緩存頁,在100次請(qǐng)求中A緩存頁被命中了20次,B緩存頁被命中了2次,很顯然A緩存頁的命中率更高,這也就意味著A在未來還會(huì)被使用到的可能性比較大,而B就會(huì)被 MySQL 認(rèn)為基本不會(huì)被使用到;
說到這里,那LRU究竟是怎么工作的。假設(shè) MySQL在將數(shù)據(jù)加載到緩存池的時(shí)候,他會(huì)將被加載進(jìn)來的緩存頁按照被加載進(jìn)來的順序插入到LRU鏈表的頭部(就是鏈表的頭插法),假設(shè) MySQL現(xiàn)在先后分別加載A、B、C數(shù)據(jù)頁到緩存頁A、B、C中,然后 LRU 的鏈表大致是這樣子的。
現(xiàn)在又來了一個(gè)請(qǐng)求,假設(shè)查詢到的數(shù)據(jù)是已經(jīng)被緩存在緩存頁B中,這時(shí)候 MySQL就會(huì)將B緩存頁對(duì)應(yīng)的描述信息插入到LRU鏈表的頭部,如下圖:
然后又來了一個(gè)請(qǐng)求,數(shù)據(jù)是已經(jīng)被緩存在了緩存頁C中,然后LRU會(huì)變成這樣子:
說到底,每次查詢數(shù)據(jù)的時(shí)候如果數(shù)據(jù)已經(jīng)在緩存頁中,那么就會(huì)將該緩存頁對(duì)應(yīng)的描述信息放到LRU鏈表的頭部,如果不在緩存頁中,就去磁盤中查找,如果查找到了,就將其加載到緩存中,并將該數(shù)據(jù)對(duì)應(yīng)的緩存頁的描述信息插入到LRU鏈表的頭部。也就是說最近使用的緩存頁都會(huì)排在前面,而排在后面的說明是不經(jīng)常被使用到的。
最后,如果 Buffer Pool 不夠使用了,那么 MySQL就會(huì)將 LRU 鏈表中的尾節(jié)點(diǎn)刷入到磁盤中,用來給 Buffer Pool 騰出內(nèi)存空間。來個(gè)整體的流程圖給大家看下
8、LRU鏈表帶來的麻煩
這里的麻煩指的是就是 MySQL本身的預(yù)讀機(jī)制帶來的問題
# 預(yù)讀機(jī)制
MySQL 在從磁盤加載數(shù)據(jù)的的時(shí)候,會(huì)將數(shù)據(jù)頁的相鄰的其他的數(shù)據(jù)頁也加載到緩存中。
# MySQL 為什么要這么做
因?yàn)楦鶕?jù)經(jīng)驗(yàn)和習(xí)慣,一般查詢數(shù)據(jù)的時(shí)候往往還會(huì)查詢?cè)摂?shù)據(jù)相鄰前后的一些數(shù)據(jù),有人可能會(huì)反問:一個(gè)數(shù)據(jù)頁上面不是就會(huì)存在該條數(shù)據(jù)相鄰的數(shù)據(jù)嗎?這可不一定,某條數(shù)據(jù)可能很大,也可能這條數(shù)據(jù)是在數(shù)據(jù)頁在頭部,也可能是在數(shù)據(jù)頁的尾部,所以 MySQL 為了提高效率,會(huì)將某個(gè)數(shù)據(jù)頁的相鄰的數(shù)據(jù)頁也加載到緩存池中。
上圖能夠看到B的相鄰也被加載到了C描述數(shù)據(jù)的前面,而實(shí)際上C的命中率比B的相鄰頁高多了,這就是LRU本身帶來的問題。
# 哪些情況會(huì)觸發(fā)預(yù)讀機(jī)制
1. 有一個(gè)參數(shù)是 innodb_read_ahead_threshold, 他的默認(rèn)值是56,意思就是如果順序的訪問了一個(gè)區(qū)里的多個(gè)數(shù)據(jù)頁,訪問的數(shù)據(jù)頁的數(shù)量超過了這個(gè)閾值,此時(shí)就會(huì)觸發(fā)預(yù)讀機(jī)制,把下一個(gè)相鄰區(qū)中的所有數(shù)據(jù)頁都加載到緩存里去(這種就是:線性預(yù)讀)
2. 如果 Buffer Pool 里緩存了一個(gè)區(qū)里的13個(gè)連續(xù)的數(shù)據(jù)頁,而且這些數(shù)據(jù)頁都是比較頻繁會(huì)被訪問的,此時(shí)就會(huì)直接觸發(fā)預(yù)讀機(jī)制,把這個(gè)區(qū)里的其他的數(shù)據(jù)頁都加載到緩存里去(這種就是:隨機(jī)預(yù)讀)隨機(jī)預(yù)讀是通過:innodb_random_read_ahead 來控制的,默認(rèn)是OFF即關(guān)閉的(MySQL 5.5已經(jīng)基本飛起該功能,應(yīng)為他會(huì)帶來不必要的麻煩,這里也不推薦大家開啟,說出來的目的是讓大家了解下有這么個(gè)東西)
還有一種情況是 SELECT * FROM students 這種直接全表掃描的,會(huì)直接加載表中的所有的數(shù)據(jù)到緩存中,這些數(shù)據(jù)基本是加載的時(shí)候查詢一次,后面就基本使用不到了,但是加載這么多數(shù)據(jù)到鏈表的頭部就將其他的經(jīng)常命中的緩存頁直接全擠到后面去了。
以上種種跡象表明,預(yù)讀機(jī)制帶來的問題還是蠻大的,既然這么大,那 MySQL為什么還要進(jìn)入預(yù)讀機(jī)制呢,說到底還是為了提高效率,**一種新的技術(shù)的引進(jìn),往往帶來新的挑戰(zhàn)**,下面我們就一起來看下 MySQL是如何解決預(yù)加載所帶來的麻煩的。
9、基于冷熱數(shù)據(jù)分離的LRU鏈表
所謂的冷熱分離,就是將 LRU 鏈表分成兩部分,一部分是經(jīng)常被使用到的熱數(shù)據(jù),另一部分是被加載進(jìn)來但是很少使用的冷數(shù)據(jù)。通過參數(shù)innodb_old_blocks_pct 參數(shù)控制的,默認(rèn)為37,也就是 37% 。用圖表示大致如下:
數(shù)據(jù)在從磁盤被加載到緩存池的時(shí)候,首先是會(huì)被放在冷數(shù)據(jù)區(qū)的頭部,然后在一定時(shí)間之后,如果再次訪問了這個(gè)數(shù)據(jù),那么這個(gè)數(shù)據(jù)所在的緩存頁對(duì)應(yīng)描述數(shù)據(jù)就會(huì)被放轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部。
那為什么說是在一定的時(shí)間之后呢,假設(shè)某條數(shù)據(jù)剛被加載到緩存池中,然后緊接著又被訪問了一次,這個(gè)時(shí)候假設(shè)就將其轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部,但是以后就再也不會(huì)被使用了,這樣子是不是就還是會(huì)存在之前的問題呢?
所以 MySQL通過innodb_old_blocks_time來設(shè)置數(shù)據(jù)被加載到緩存池后的多少時(shí)間之后再次被訪問,才會(huì)將該數(shù)據(jù)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部,該參數(shù)默認(rèn)是1000單位為:毫秒,也就是1秒之后,如果該數(shù)據(jù)又被訪問了,那么這個(gè)時(shí)候才會(huì)將該數(shù)據(jù)從 LRU 鏈表的冷數(shù)據(jù)區(qū)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)。
現(xiàn)在再回頭看下上面的問題
# 通過預(yù)加載(加載相鄰數(shù)據(jù)頁)進(jìn)來的數(shù)據(jù)
1. 這個(gè)時(shí)候就很好理解了,反正數(shù)據(jù)會(huì)被放在LRU鏈表的冷數(shù)據(jù)區(qū)的(注意:這里說的放在鏈表中的數(shù)據(jù)都是指的是<緩存頁中的數(shù)據(jù)所對(duì)應(yīng)的描述數(shù)據(jù)>),當(dāng)在指定時(shí)候之后,如果某些緩存頁被訪問了那么就將該緩存頁的描述數(shù)據(jù)放到熱數(shù)據(jù)區(qū)鏈表的頭部
# 全表掃描加載進(jìn)來的數(shù)據(jù)頁
1. 和上面一樣,數(shù)據(jù)都是先在冷數(shù)據(jù)區(qū),然后在一定時(shí)間之后,再次被訪問到的數(shù)據(jù)頁才會(huì)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)的鏈表的頭結(jié)點(diǎn),所以這也就很好的解決了全表掃描所帶來的問題
再來思考下 Buffer Pool 內(nèi)存不夠的問題
# Buffer Pool 內(nèi)存空間不夠使用了怎么辦?也就是說沒有足夠使用的空閑的緩存頁了。
1. 這個(gè)問題在這個(gè)時(shí)候就顯得非常簡單了,直接將鏈表冷數(shù)據(jù)區(qū)的尾節(jié)點(diǎn)的描述數(shù)據(jù)多對(duì)應(yīng)的緩存頁刷到磁盤即可。
但是這樣子還不是足夠完美,為什么這么說,剛剛我們一直在討論的是冷數(shù)據(jù)區(qū)的數(shù)據(jù)被訪問,然后在一定規(guī)則之下會(huì)被加載到熱數(shù)據(jù)鏈表的頭部,但是現(xiàn)在某個(gè)請(qǐng)求需要訪問的數(shù)據(jù)就在熱數(shù)據(jù)區(qū),那是不是直接把該數(shù)據(jù)所在的緩存頁對(duì)應(yīng)的描述數(shù)據(jù)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表頭部呢?
很顯然不是這樣子的,因?yàn)闊釘?shù)據(jù)區(qū)的數(shù)據(jù)本身就是會(huì)被頻繁訪問的,這樣子如果每次訪問都去移動(dòng)鏈表,勢必造成性能的下降(影響再小極端情況下也可能會(huì)不可控),所以 MySQL針對(duì)熱數(shù)據(jù)區(qū)的數(shù)據(jù)的轉(zhuǎn)移也有相關(guān)的規(guī)則。
該規(guī)則就是:如果被訪問的數(shù)據(jù)所在的緩存頁在熱數(shù)據(jù)區(qū)的前25%,那么該緩存頁對(duì)應(yīng)的描述數(shù)據(jù)是不會(huì)被轉(zhuǎn)移到熱數(shù)據(jù)鏈表的頭部的,只有當(dāng)被訪問的緩存頁對(duì)應(yīng)的描述數(shù)據(jù)在熱數(shù)據(jù)區(qū)鏈表的后75%,該緩存頁的描述數(shù)據(jù)才會(huì)被轉(zhuǎn)移到熱數(shù)據(jù)鏈表的頭部。
舉個(gè)例子來說,假設(shè)熱數(shù)據(jù)區(qū)有100個(gè)緩存頁(這里的緩存頁還是指的是緩存頁對(duì)應(yīng)的描述數(shù)據(jù),再強(qiáng)調(diào)下,鏈表中存放的是緩存頁的描述數(shù)據(jù),為了方便有時(shí)候會(huì)直接說緩存頁。希望朋友們注意),當(dāng)被訪問的緩存頁在前25個(gè)的時(shí)候,熱數(shù)據(jù)區(qū)的鏈表是不會(huì)有變化的,當(dāng)被訪問的緩存頁在26~100(也就是數(shù)據(jù)在熱數(shù)據(jù)區(qū)鏈表的后75%里面)的時(shí)候,這個(gè)時(shí)候被訪問的緩存頁才會(huì)被轉(zhuǎn)移到鏈表的頭部。
到此為止, MySQL對(duì)于LUR 鏈表的優(yōu)化就堪稱完美了。是不是看到這里瞬間感覺很多東西都明朗了,好了,對(duì)于 LRU 鏈表我們就討論到這里了。
10、Buffer Pool 中的鏈表小結(jié)
# free鏈表
用來存放空閑的緩存頁的描述數(shù)據(jù),如果某個(gè)緩存頁被使用了,那么該緩存頁對(duì)應(yīng)的描述數(shù)據(jù)就會(huì)被從free鏈表中移除
# flush鏈表
被修改的臟數(shù)據(jù)都記錄在 Flush 中,同時(shí)會(huì)有一個(gè)后臺(tái)線程會(huì)不定時(shí)的將 Flush 中記錄的描述數(shù)據(jù)對(duì)應(yīng)的緩存頁刷新到磁盤中,如果某個(gè)緩存頁被刷新到磁盤中了,那么該緩存頁對(duì)應(yīng)的描述數(shù)據(jù)會(huì)從 Flush 中移除,同時(shí)也會(huì)從LRU鏈表中移除(因?yàn)樵摂?shù)據(jù)已經(jīng)不在 Buffer Pool 中了,已經(jīng)被刷入到磁盤,所以就也沒必要記錄在 LRU 鏈表中了),同時(shí)還會(huì)將該緩存頁的描述數(shù)據(jù)添加到free鏈表中,因?yàn)樵摼彺骓撟兊每臻e了。
# LRU鏈表
數(shù)據(jù)頁被加載到 Buffer Pool 中的對(duì)應(yīng)的緩存頁后,同時(shí)會(huì)將緩存頁對(duì)應(yīng)的描述數(shù)據(jù)放到 LRU 鏈表的冷數(shù)據(jù)的頭部,當(dāng)在一定時(shí)間過后,冷數(shù)據(jù)區(qū)的數(shù)據(jù)被再次訪問了,就會(huì)將其轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部,如果被訪問的數(shù)據(jù)就在熱數(shù)據(jù)區(qū),那么如果是在前25%就不會(huì)移動(dòng),如果在后75%仍然會(huì)將其轉(zhuǎn)移到熱數(shù)據(jù)區(qū)鏈表的頭部
后臺(tái)線程將冷數(shù)據(jù)區(qū)的尾節(jié)點(diǎn)的描述數(shù)據(jù)對(duì)應(yīng)的緩存頁刷入磁盤文件中
11、Buffer Pool 的并發(fā)性能
我們平時(shí)的系統(tǒng)絕對(duì)不可能每次只有一個(gè)請(qǐng)求來訪問的,說白了就是如果多個(gè)請(qǐng)求同時(shí)來執(zhí)行增刪改,那他們會(huì)并行的去操作 Buffer Pool 中的各種鏈表嗎?如果是并行的會(huì)不會(huì)有什么問題。
實(shí)際上 MySQL在處理這個(gè)問題的時(shí)候考慮的非常簡單,就是: Buffer Pool 一次只能允許一個(gè)線程來操作,一次只有一個(gè)線程來執(zhí)行這一系列的操作,因?yàn)镸ySQL 為了保證數(shù)據(jù)的一致性,操作的時(shí)候必須緩存池加鎖,一次只能有一個(gè)線程獲取到鎖。
這個(gè)時(shí)候,大家這時(shí)候肯定滿腦子問號(hào)。串行那還談什么效率?大家別忘記了,這一系列的操作都是在內(nèi)存中操作的,實(shí)際上這是一個(gè)瞬時(shí)的過程,在內(nèi)存中的操作基本是幾毫秒的甚至微妙級(jí)別的事情。
但是話又說回來,串行執(zhí)行再怎么快也是串行,雖然不是性能瓶頸,這還有更好的優(yōu)化辦法嗎?那肯定的 MySQL早就設(shè)計(jì)好了這些規(guī)則。那就是 Buffer Pool 是可以有多個(gè)的,可以通過 MySQL的配置文件來配置,參數(shù)分別是:
# Buffer Pool 的總大小
innodb_buffer_pool_size=8589934592
# Buffer Pool 的實(shí)例數(shù)(個(gè)數(shù))
innodb_buffer_pool_instance=4
一般在生產(chǎn)環(huán)境中,在硬件不緊張的情況下,建議使用此策略。這個(gè)時(shí)候大家是不是又會(huì)有一個(gè)疑問(如果沒有那說明你沒認(rèn)真思考哦),大家應(yīng)該有這樣的疑問:
# 問:多個(gè) Buffer Pool 所帶來的問題思考
在多個(gè)線程訪問不同的 Buffer Pool 那不同的線程加載的數(shù)據(jù)必然是在不同的 Buffer Pool 中,假設(shè) A 線程加載數(shù)據(jù)頁A到 Buffer Pool A 中,B 線程加載數(shù)據(jù)頁B到 Buffer Pool B 中,然后兩個(gè)都執(zhí)行完了,這個(gè)時(shí)候 C 線程來了,他到達(dá)的是 Buffer Pool B中,但是 C 要訪問的數(shù)據(jù)是在 Buffer Pool A中的數(shù)據(jù)頁上了,這個(gè)時(shí)候 C 還會(huì)去加載數(shù)據(jù)頁A嗎?,這種情況會(huì)發(fā)生嗎?在不同的 Buffer Pool 緩存中會(huì)去緩存相同的數(shù)據(jù)頁嗎?
# 答:多個(gè) Buffer Pool 所帶來的問題解答
這種情況很顯然不會(huì)發(fā)生,既然不會(huì)發(fā)生,那 MySql 是如何解決這種問題的?其實(shí)前面已經(jīng)提到過了,那就是 數(shù)據(jù)頁緩存哈希表(看下圖),里面存放的是表空間號(hào)+數(shù)據(jù)頁號(hào) = 緩存頁地址,所以 MySQL 在加載數(shù)據(jù)所在的數(shù)據(jù)頁的時(shí)候根據(jù)這一系列的映射關(guān)系判斷數(shù)據(jù)頁是否被加載,被加載到了那個(gè)緩存頁中,所以 MySQL 能夠精確的確定某個(gè)數(shù)據(jù)頁是否被加載,被加載的到了哪個(gè)緩存頁,絕不可能出現(xiàn)重復(fù)加載的情況。
12、動(dòng)態(tài)調(diào)整 Buffer Pool 的大小
到此為止,本文已經(jīng)詳細(xì)的介紹了 Buffer Pool 的內(nèi)存結(jié)構(gòu),它的數(shù)據(jù)是如何存放的,如何刷磁盤的,又是如何加載的,以什么樣的形式存在的等等知識(shí)點(diǎn),下面我們繼續(xù)挖掘,將 Buffer Pool 的相關(guān)知識(shí)點(diǎn)一次說個(gè)夠。我們現(xiàn)在來討論下 Buffer Pool 的大小能否動(dòng)態(tài)調(diào)整。
假設(shè)我們現(xiàn)在的 Buffer Pool 的大小是 2GB大小,現(xiàn)在想將其擴(kuò)大到 4GB,現(xiàn)在說一下如果真的要這么做,我們的 MySq 需要做哪些事情。首先 ,MySQL 需要向操作系統(tǒng)申請(qǐng)一塊大小為 4G 的連續(xù)的地址連續(xù)的內(nèi)存空間,然后將原來的 Buffer Pool 中的數(shù)據(jù)拷貝到新的 Buffer Pool 中。
這樣可能嗎?如果原來的是8G,擴(kuò)大到 16G,那這個(gè)將原來的數(shù)據(jù)復(fù)制到新的 Buffer Pool 中是不是極為耗時(shí)的,所以這樣的操作 MySQL必然是不支持的。但實(shí)際上這樣的需求是客觀存在的,那 MySQL是如何解決的呢?
為了處理這種情況,MySQL設(shè)計(jì)出 chunk (http 協(xié)議中也有使用到這個(gè)思想,所以我們會(huì)發(fā)現(xiàn)很多技術(shù)的優(yōu)秀思想都是在相互借鑒)機(jī)制來解決的
# 什么是chunk機(jī)制
chunk是 MySQL 設(shè)計(jì)的一種機(jī)制,這種機(jī)制的原理是將 Buffer Pool 拆分一個(gè)一個(gè)大小相等的 chunk 塊,每個(gè) chunk 默認(rèn)大小為 128M(可以通過參數(shù)innodb_buffer_pool_chunk_size 來調(diào)整大小),也就是說 Buffer Pool 是由一個(gè)個(gè)的chunk組成的
假設(shè) Buffer Pool 大小是2GB,而一個(gè)chunk大小默認(rèn)是128M,也就是說一個(gè)2GB大小的 Buffer Pool 里面由16個(gè) chunk 組成,每個(gè)chunk中有自己的緩存頁和描述數(shù)據(jù),而 free 鏈表、flush 鏈表和 lru 鏈表是共享的
如果說有多個(gè) Buffer Pool ,那就是這樣
說到這里好像還是沒有說到 MySQL到底是如何通過 chunk 機(jī)制來調(diào)整大小的。實(shí)際上是這樣的,假設(shè)現(xiàn)在 Buffer Pool 有 2GB,里面有16個(gè)chunk,現(xiàn)在想要擴(kuò)大到 4GB,那么這個(gè)時(shí)候只需要新申請(qǐng)一個(gè)個(gè)的 chunk 就可以了。
這樣不但不需要申請(qǐng)一塊很大的連續(xù)的空間,更不需要將復(fù)制數(shù)據(jù)。這樣就能達(dá)到動(dòng)態(tài)調(diào)整大小了(不會(huì)還有人問:這只是擴(kuò)大,怎么縮小呢?gun)。不得不說 MySQL真機(jī)智。
13、生產(chǎn)環(huán)境如何設(shè)置 Buffer Pool 大小
Buffer Pool 是不是越大越好,理論上是的。那如果一個(gè)機(jī)器內(nèi)存是16GB那分配給 Buffer Pool 15GB,這樣很顯然是不行的,因?yàn)椴僮飨到y(tǒng)要占內(nèi)存,你的機(jī)器上總會(huì)運(yùn)行其他的進(jìn)行的吧?那肯定也是需要占用內(nèi)存的。根據(jù)很多實(shí)際生產(chǎn)經(jīng)驗(yàn)得出得比較合理的大小是機(jī)器內(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 一共有多少個(gè)緩存頁
Buffer Pool size
-- free 鏈表中一共有多少個(gè)緩存也是可以使用的
Free buffers
-- lru鏈表中一共有多少個(gè)緩存頁
Database pages
-- lru鏈表鏈表中的冷數(shù)據(jù)區(qū)一共有多少個(gè)緩存頁
Old database pages
-- flush鏈表中的緩存頁的數(shù)量
Modified db pages
-- 等待從磁盤上加載進(jìn)來的緩存頁的數(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)被訪問但是沒有進(jìn)入到熱數(shù)據(jù)區(qū)的緩存頁的數(shù)量
Pages made young 260368814, not young 0
-- 每秒從冷數(shù)據(jù)轉(zhuǎn)移到熱數(shù)據(jù)區(qū)的緩存頁的數(shù)量,以及每秒在冷數(shù)據(jù)區(qū)被訪問但是沒有進(jìn)入熱數(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é)束語
本篇文章我們?cè)敿?xì)討論了 Buffer Pool 的內(nèi)存結(jié)構(gòu),從 free 鏈表到 lru 鏈表,從 Buffer Pool 到 chunk,從磁盤中加載一個(gè)數(shù)據(jù)頁到 Buffer Pool 到最后該數(shù)據(jù)頁又被刷回到磁盤中的一整個(gè)過程,他的每一步都做了什么。
我們一起討論完本文以后,是不是瞬間有種看透來了 MySQL的感覺,但是這個(gè)僅僅是前提,學(xué)習(xí)這些的目的是為了更好的理解 MySQL讓我們能夠在工作中更加游刃有余地使用它。因?yàn)橹挥性谥懒说讓釉淼那闆r下,才能熟悉他的工作原理,遇到問題才能對(duì)癥下藥。






