大綱
1.Buffer Pool是什麼
2.如何配置Buffer Pool的大小
3.資料頁是MySQL中抽象出來的資料單位
4.資料頁如何對應Buffer Pool中的快取頁
5.快取頁對應的描述資訊是什麼
6.Buffer Pool簡單總結
7.資料庫啟動時如何初始化Buffer Pool
8.free連結串列可判斷哪些快取頁是空閒的
9.free連結串列佔用多少記憶體空間
10.如何讀取資料頁到Buffer Pool的快取頁
11.如何知道資料頁有沒有被快取
12.空閒快取頁與free連結串列總結
13.Buffer Pool中會不會有記憶體碎片
14.髒資料到底為什麼會髒
15.flush連結串列可判斷哪些快取頁是髒頁
16.flush連結串列的虛擬碼
17.flush連結串列和髒頁總結
18.如果Buffer Pool中的快取頁不夠了怎麼辦
19.淘汰快取頁與快取命中率
20.引入LRU連結串列來判斷哪些快取頁是不常用的
21.基於冷熱資料分離思想設計LRU連結串列
22.Buffer Pool的快取頁以及幾個連結串列總結
23.LRU連結串列冷資料區域的快取頁何時刷盤
24.Buffer Pool在訪問時是否需要加鎖
25.多個Buffer Pool最佳化併發能力
26.透過chunk動態調整執行期的Buffer Pool
27.生產環境應給Buffer Pool設定多少記憶體、多少Buffer Pool、多大的chunk
28.show engine innodb status輸出詳解
關鍵字:MySQL記憶體資料的更新機制
1.Buffer Pool是什麼
Buffer Pool是MySQL資料庫中一個非常關鍵的元件。資料庫中的資料最終都是存放在磁碟檔案上的。但是在對資料庫執行增刪改查操作時,不可能直接更新磁碟上的資料。因為如果直接對磁碟進行隨機讀寫操作,那速度是相當的慢的。隨便一個大磁碟檔案的隨機讀寫操作,可能都要幾百毫秒,這樣資料庫每秒也就只能處理幾百個請求。
資料庫執行增刪改操作時,是基於記憶體Buffer Pool中的資料進行的。同時為了防止在更新完記憶體中的資料之後,由於機器當機而造成資料丟失,資料庫引入了redo日誌機制,即增刪改時會把修改也寫入redo日誌中。
Buffer Pool就是資料庫的一個記憶體元件,裡面快取了磁碟上的真實資料。當執行更新時,會寫undo日誌、修改Buffer Pool資料、寫redo日誌;當提交事務時,會將redo日誌刷磁、binlog刷盤、新增commit標記。最後後臺IO執行緒會隨機把Buffer Pool裡的髒資料刷入到磁碟資料檔案中。
2.如何配置Buffer Pool的大小
由於Buffer Pool本質就是資料庫的一個記憶體元件,所以Buffer Pool是有大小的,不能無限大。
Buffer Pool的預設大小是128MB,有點偏小。在實際生產環境下可以對Buffer Pool進行調整。比如對於16核32GB的資料庫,可以給Buffer Pool分配2GB大小的記憶體。
[server]
innodb_buffer_pool_size = 2147483648
3.資料頁是MySQL中抽象出來的資料單位
MySQL是如何將資料放在Buffer Pool中的?我們日常使用的資料庫的資料模型是表 + 欄位 + 行。資料庫裡有一個個表,一個表有很多欄位,一個表有很多行資料。所以資料是否是一行一行地放在Buffer Pool裡面的?
MySQL會把很多行資料放在一個資料頁裡。然後磁碟檔案中會有很多資料頁,每一頁放了很多行資料。
假設要更新一行資料,此時資料庫會找到這行資料所在的資料頁。然後從磁碟檔案中把這行資料所在的資料頁載入到Buffer Pool裡。因此Buffer Pool中存放的是一個一個的資料頁。
4.資料頁如何對應Buffer Pool中的快取頁
預設情況下,磁碟中存放的資料頁大小是16KB。Buffer Pool中存放的一個個資料頁,通常叫做快取頁。因為Buffer Pool是一個緩衝池,裡面的資料都是從磁碟載入到記憶體的。所以Buffer Pool中一個快取頁的大小等於磁碟上一個資料頁的大小,都是16KB。
5.快取頁對應的描述資訊是什麼
每個快取頁都有對應的描述資訊,如快取頁所屬表空間、資料頁編號、快取頁在Buffer Pool的地址等。
每個快取頁的描述資訊本身也是一塊資料。每個快取頁的描述資料放在Buffer Pool最前面,然後各個快取頁放在後面。
Buffer Pool中的描述資料大概相當於快取頁大小的5%,也就是每個描述資料佔大概是16KB/20=800個位元組。
如果設定了Buffer Pool的大小是128MB,則實際上Buffer Pool真正的大小可能是135MB,因為每個快取頁還有對應的描述資料。因此,Buffer Pool的結構看起來像如下的樣子:
6.Buffer Pool簡單總結
一.緩衝池Buffer Pool的預設大小是128MB
緩衝池Buffer Pool的大小根據伺服器的配置來調整。比如伺服器的配置是16核32GB,可以給緩衝池Buffer Pool分配2GB記憶體。
二.資料頁是MySQL抽象出來的資料單位
磁碟檔案中有很多資料頁,每一頁中放了很多行資料。如果資料庫要更新某一行資料,首先會找到這行資料所在的資料頁,然後把這行資料頁載入到緩衝池Buffer Pool中。緩衝池Buffer Pool中存放的一個一個的資料頁,也被稱為快取頁。資料頁預設大小為16KB,資料頁和快取頁的大小是一樣的。
三.Buffer Pool中每個快取頁都有對應的描述資料
描述資料包括:快取頁所屬的表空間、資料頁的編號、快取頁在Buffer Pool中的地址等。每個快取頁的描述資料放Buffer Pool最前面,各個快取頁放在後面。Buffer Pool裡的一個描述資料大小相當於一個快取頁的5%,約800位元組。
7.資料庫啟動時如何初始化Buffer Pool
資料庫的Buffer Pool裡會包含很多個快取頁,同時每個快取頁還有對應的描述資料。
資料庫啟動時,會按照設定的Buffer Pool大小,去作業系統申請一塊記憶體區域,作為Buffer Pool的記憶體區域。申請完畢後,資料庫會按照預設的快取頁大小及對應的描述資料大小,在Buffer Pool中劃分一個個快取頁和對應的描述資料。
然後當資料庫把Buffer Pool劃分完畢後,裡面的快取頁都是空的。需要等資料庫執行起來後執行增刪改查操作時:才會把對應的資料頁從磁碟裡讀取出來,放入Buffer Pool中的快取頁裡。
8.free連結串列可判斷哪些快取頁是空閒的
當資料庫執行起來後,肯定會不停地進行增刪改查操作。此時會從磁碟上讀取一個個的資料頁放入到Buffer Pool中的快取頁裡。
預設情況下,磁碟上的資料頁和快取頁是一一對應的,都是16KB。Buffer Pool把資料快取起來後,就可以對資料在記憶體裡執行增刪改查。
但是當資料庫從磁碟上讀取資料頁放入Buffer Pool中的快取頁時,首先需要解決一個問題:哪些快取頁是空閒的?
為此,資料庫為Buffer Pool設計了一個free連結串列,它是一個雙向連結串列。在這個free連結串列裡,每個節點就是一個空閒快取頁的描述資料塊的地址。只要一個快取頁是空閒的,則其描述資料塊的地址就會被放入free連結串列中。所以資料庫剛啟動時,如果此時所有的快取頁都是空閒的,那麼所有快取頁的描述資料塊就會被放進該free連結串列裡。
下圖展示了一個free連結串列,這個free連結串列裡的元素就是各個快取頁的描述資料塊。只要快取頁是空閒的,則快取頁對應的描述資料塊就會加入到free連結串列中。每個節點都會雙向連結自己的前後節點,組成一個雙向連結串列。
此外,這個free連結串列還有一個基礎節點。該基礎節點會引用連結串列的頭節點和尾節點,以及儲存連結串列中的描述資料塊節點數,也就是會有多少個空閒的快取頁。
9.free連結串列佔用多少記憶體空間
描述資料塊,在Buffer Pool裡有一份,在free連結串列裡也有一份。這樣是不是記憶體裡就出現了兩個一模一樣的描述資料塊了?
其實不是的。因為這個free連結串列,本身就是由Buffer Pool裡的描述資料塊組成的。可以認為每個描述資料塊都有兩個指標,一個free_pre、一個free_next。這兩個指標分別指向自己在free連結串列的上一個節點、以及下一個節點。
透過Buffer Pool中的描述資料塊的free_pre和free_next兩個指標,就可以把所有的描述資料塊串成一個free連結串列(雙向連結串列)。
對於free連結串列而言,只有一個基礎節點是不屬於Buffer Pool的。基礎節點是40位元組大小的一個節點。裡面存放了free連結串列的頭節點和尾節點地址、及當前連結串列裡有多少個節點。
10.如何讀取資料頁到Buffer Pool的快取頁
首先需要從free連結串列裡獲取一個描述資料塊,然後就可以獲取到這個描述資料塊對應的空閒快取頁,接著把磁碟上的資料頁讀取到該空閒快取頁裡去,同時把相關的一些描述資料寫入該空閒快取頁的描述資料塊裡,最後把那個描述資料塊從free連結串列裡移除。
從free連結串列移除一個描述資料塊的為程式碼如下。假設有一個描述資料塊02,它的上一個節點是描述資料塊01,下一個節點是描述資料塊03,那麼描述資料塊02的結構是:
//描述資料塊
DescriptionDataBlock {
//這個塊就是block02
block_id = block02
//在free連結串列中的上一個節點是block01
free_pre = block01;
//在free連結串列中的下一個節點是block03
free_next = block03;
}
現在尾節點block03被使用了,要從free連結串列中移除,那麼此時可以直接把block02節點的free_next設定為null即可:
//描述資料塊
DescriptionDataBlock {
//這個塊就是block02
block_id = block02
//在free連結串列中的上一個節點是block01
free_pre = block01;
//在free連結串列中的下一個節點是空的
free_next = null;
}
11.如何知道資料頁有沒有被快取
我們在執行增刪改查時,首先要看這個資料頁有沒有被快取:如果已被快取,則直接使用;如果沒有被快取,就從free連結串列中找到一個空閒的快取頁。然後從磁碟上讀取資料頁寫入快取頁,同時寫入描述資料,接著在free連結串列中移除該描述資料塊。
所以為了判斷資料頁有沒有被快取,InnoDB會有一個雜湊表。這個雜湊表使用表空間號+資料頁號作為key,而快取頁地址作為value。當要使用一個資料頁時,透過表空間號+資料頁號作為key去雜湊表查詢。如果雜湊表中沒有就讀取磁碟的資料頁,如果有則說明資料頁已被快取。
因此,資料庫每次讀取資料頁到快取後,都會往雜湊表中寫入一個kv對。key就是表空間號+資料頁號,value就是快取頁地址。這樣下次如果要使用該資料頁,就可以從雜湊表裡直接讀取出快取頁地址,然後根據快取頁地址到Buffer Pool中讀取出具體的快取頁資料。
12.空閒快取頁與free連結串列總結
(1)資料庫啟動時會按照設定的Buffer Pool大小向OS申請記憶體
當資料庫向OS申請到設定的Buffer Pool大小的記憶體後,就會在緩衝池中劃分出一個個空閒快取頁和相應的描述資料塊。
(2)Buffer Pool有一個叫free連結串列的雙向連結串列
free連結串列的每個節點是一個空閒快取頁的描述資料塊的地址,透過free連結串列可知哪些快取頁是空閒的。
(3)根據free連結串列的節點可得到一個空閒快取頁
從free連結串列中獲取一個節點後,根據該節點就能找到對應的空閒快取頁。接著就可以將磁碟中的資料頁讀取到該空閒快取頁裡。同時把該資料頁的描述資料寫到該空閒快取頁對應的描述資料塊裡。以及把表空間號 + 資料頁號作為key,快取頁地址作為value,寫到雜湊表。這樣下次讀取該資料頁時可透過key查雜湊表,直接從緩衝池裡進行讀取。
(4)增刪改查一條資料時InnoDB引擎會怎麼處理
首先InnoDB會獲取到對應資料的"表空間號 + 資料頁號"。然後根據"表空間號 + 資料頁號"作為key,去雜湊表中進行查詢。如果能查到快取頁地址,則去Buffer Pool中讀取對應的快取頁資料。如果雜湊表查不到,則說明要將磁碟的資料頁讀取到緩衝區的快取頁裡。
於是會先從free連結串列裡獲取一個節點,然後找到其描述資料塊的地址。透過該地址可得到一個空閒快取頁,就能把資料頁讀取到該空閒快取頁裡。同時會把描述資料也寫到該快取頁的描述資料塊裡,以及把表空間號 + 資料頁號作為key,快取頁地址作為value,寫到雜湊表。
(5)SQL語句中表和行、表空間和資料頁的關係
一.表、列和行都是邏輯概念
資料庫裡有一個表,表裡有幾個欄位有多少行,這些都是邏輯上的概念。我們作為資料庫使用方,並不關注它們具體在資料庫磁碟怎麼儲存。
二.表空間、資料頁都是物理概念
在物理層面,表裡的資料都放在一個表空間中。表空間由一堆磁碟上的資料檔案組成,這些檔案裡都存放了表的資料。而這些資料又是由一個個的資料頁組織起來的。所以表空間、資料頁是物理層面的概念。
13.Buffer Pool中會不會有記憶體碎片
Buffer Pool中會有記憶體碎片。由於Buffer Pool大小是可以設定的,所以Buffer Pool劃分完整的快取頁和描述資料塊後,可能還剩一點記憶體。而這點記憶體放不下任何一個快取頁,只能放著不能用,這就是記憶體碎片。
如何減少記憶體碎片?
資料庫在Buffer Pool中劃分快取頁時,會讓所有快取頁和描述資料塊都緊密挨在一起,這樣就能儘可能減少記憶體浪費、減少記憶體碎片。如果Buffer Pool裡的快取頁是東一塊西一塊,則必然導致快取頁的記憶體間有很多記憶體空隙,從而產生大量記憶體碎片。
14.髒資料到底為什麼會髒
MySQL在執行增刪改語句時,如果在雜湊表中發現資料頁沒有快取,則會基於free連結串列找到一個空閒的快取頁,然後將資料頁讀取到快取頁裡。如果在雜湊表中發現資料頁已快取,那麼會直接使用快取頁。
因此,無論如何,要更新的資料頁都會在Buffer Pool的快取頁裡。MySQL是基於Buffer Pool記憶體來執行具體的增刪改查操作的。
所以,當MySQL去更新Buffer Pool的快取頁中的資料時,一旦更新完,則快取頁裡的資料和磁碟上資料頁的資料就不一致了。這時就說該快取頁是髒資料,或者髒頁。
15.flush連結串列可判斷哪些快取頁是髒頁
在Buffer Pool裡,有些快取頁經過修改是髒頁,有些則只有查而不是髒頁。所以為了方便資料庫從快取頁中區分出髒頁,資料庫引入了一個跟free連結串列類似的flush連結串列。
flush連結串列也是透過快取頁的描述資料塊中的兩個指標,讓被修改過的快取頁的描述資料塊組成一個雙向連結串列的,其中這兩個指標分別是flush_pre、flush_next。
凡是被修改過的快取頁,都會把它的描述資料塊加入到flush連結串列中。flush的意思就是這些都是髒頁,後續都是要flush重新整理到磁碟上去的。
16.flush連結串列的虛擬碼
下面用虛擬碼來展示一下這個flush連結串列的構造過程。比如現在快取頁01被修改了資料,那麼它就是髒頁了,此時就必須把它加入到flush連結串列中。
假設快取頁01的描述資料塊如下所示:
//描述資料塊
DescriptionDataBlock {
//這是快取頁01的資料塊
block_id = block01
//在free連結串列中的上一個節點和下一個節點(由於這個快取頁已經儲存了資料頁放在了緩衝池裡, 所以肯定不在free連結串列了)
//所以free連結串列中的兩個指標都是null
free_pre = null
free_next = null
//在flush連結串列中的上一個節點和下一個節點
//現在因為flush連結串列中就它一個節點, 所以也是null
flush_pre = null
flush_next = null
}
//flush連結串列的基礎節點
FlushLinkListBaseNode {
//基礎節點指向連結串列起始節點和結束節點的指標
//flush連結串列中目前就一個快取頁01, 所以指向它的描述資料塊
start = block01
end = block01
//flush連結串列中有幾個節點
count = 1
}
現在flush連結串列的基礎節點就指向了一個block01的節點。假如接下來快取頁02也被更新了,這時候快取頁02也是髒頁。那麼快取頁02的描述資料塊也要被加入到flush連結串列中去。
//描述資料塊
DescriptionDataBlock {
//這是快取頁01的資料塊
block_id = block01
//在free連結串列中的上一個節點和下一個節點(由於這個快取頁已經儲存了資料頁放在了緩衝池裡, 所以肯定不在free連結串列了)
//所以free連結串列中的兩個指標都是null
free_pre = null
free_next = null
//在flush連結串列中的上一個節點和下一個節點
//現在因為flush連結串列中就它是起始節點, 所以它的flush_pre指標是null
flush_pre = null
//然後flush連結串列中它的下一個節點是block02, 所以flush_next指向block02
flush_next = block02
}
//描述資料塊
DescriptionDataBlock {
//這是快取頁02的資料塊
block_id = block02
//在free連結串列中的上一個節點和下一個節點(由於這個快取頁已經儲存了資料頁放在了緩衝池裡, 所以肯定不在free連結串列了)
//所以free連結串列中的兩個指標都是null
free_pre = null
free_next = null
//在flush連結串列中的上一個節點和下一個節點
//現在因為flush連結串列中就它是尾節點, 所以它的上一個節點是block01, 它的下一個節點是null
flush_pre = block01
flush_next = null
}
由此可見,當資料庫更新快取頁時,透過變換快取頁中的描述資料塊的flush連結串列的指標,可以把髒頁的描述資料塊組成一個雙向連結串列,也就是flush連結串列。flush連結串列的基礎節點會指向起始節點和尾節點。透過flush連結串列,就可記錄哪些快取頁是髒頁了。
17.flush連結串列和髒頁總結
一.透過free連結串列來管理所有空閒的資料頁
載入磁碟的資料頁時,先透過free連結串列拿到空閒的快取頁地址,然後再把磁碟的資料頁寫到這個Buffer Pool中的快取頁裡。
二.透過雜湊表來管理所有在Buffer Pool快取過的資料頁
根據雜湊表可以快速在Buffer Pool中查詢出快取的資料頁。
三.透過flush連結串列來管理所有被更新後的等待被刷盤的快取頁
free連結串列和flush連結串列都透過使用地址指標來大大減少記憶體的佔用。free連結串列和flush連結串列的節點都是由快取頁的描述資料塊來實現的。free連結串列和flush連結串列都透過兩個指標來構成雙向連結串列。
18.如果Buffer Pool中的快取頁不夠了怎麼辦
上面介紹了:Buffer Pool中快取頁的劃分、free連結串列的使用、資料頁是如何載入到快取頁、對快取頁修改後flush連結串列如何記錄髒頁。
當資料庫執行增刪改查時,都會把磁碟上的資料頁載入到快取頁裡。而且在載入過程中必然要把磁碟的資料頁載入到空閒的快取頁裡。所以才會首先從free連結串列中找一個空閒的快取頁,然後把磁碟上的資料頁載入到該空閒的快取頁裡。
隨著資料庫不停地把磁碟上的資料頁載入到空閒的快取頁裡,free連結串列中的空閒快取頁就會越來越少,直到free連結串列沒有空閒快取頁;這時就要淘汰掉一些快取頁。
淘汰快取頁:也就是把一個快取頁裡被修改過的資料刷到磁碟的資料頁裡,然後這個快取頁就可以清空了,讓它重新變成一個空閒的快取頁。既然要把快取頁的資料刷入磁碟,那麼應該把哪些快取頁的資料刷入磁碟?
19.淘汰快取頁與快取命中率
要處理"應該把哪些快取頁的資料給刷入磁碟"的問題,就需要快取命中率。
假設現在有兩個快取頁。一個快取頁的資料,經常被修改和查詢。比如在100次請求中有30次都是在查詢和修改這個快取頁裡的資料,那麼此時可認為這個快取頁的資料的快取命中率很高。另一個快取頁的資料,偶爾被修改和查詢。比如從磁碟載入到快取頁後的100次請求只修改和查詢過1次,那麼此時可認為這個快取頁的資料的快取命中率有點低。
這時傾向於將命中率低的快取頁進行淘汰,讓經常訪問的快取頁留下來,淘汰掉很少被訪問的快取頁。
20.引入LRU連結串列來判斷哪些快取頁是不常用的
(1)簡單LRU連結串列的工作原理
(2)簡單LRU連結串列可能存在的預讀問題
(3)觸發MySQL預讀機制的情況
(4)簡單LRU連結串列可能存在的全表掃描問題
(5)總結
要知道哪些快取頁經常被訪問、哪些快取頁很少被訪問,可藉助LRU連結串列。LRU就是Least Recently Used,最近最少使用的意思。
(1)簡單LRU連結串列的工作原理
假設InnoDB從磁碟載入一個資料頁到快取頁時,就把這個快取頁的描述資料塊放到LRU連結串列頭部去。
那麼只要一個快取頁有資料,那麼該快取頁就會在LRU裡。並且最新載入資料的快取頁,會被放到LRU連結串列的頭部。
假設某個快取頁的描述資料塊本來在LRU連結串列的尾部,後面只要查詢或者修改了這個快取頁的資料,也會把其描述資料塊挪動到LRU連結串列頭部。
總之,就是保證最近被訪問過的快取頁,一定在LRU連結串列的頭部。這樣當緩衝區沒有空閒的快取頁時,可以在LRU連結串列尾部找一個快取頁。而這個快取頁就是最近最少被訪問的那個快取頁。然後把LRU連結串列尾部的那個快取頁刷入磁碟從而騰出一個空閒的快取頁,最後把需要的磁碟資料頁載入到這個空閒的快取頁中即可。
這個LRU連結串列需要一定長度,不能只有2個節點。否則如果先是節點1被訪問100次,接著到節點2被訪問。這樣雖然連結串列尾部是節點1,但實際上節點1是最近最少被訪問的。
(2)簡單LRU連結串列可能存在的預讀問題
在LRU連結串列的尾部,一定是最近最少被訪問的那個快取頁。但這個LRU機制在實際執行中,面對MySQL的預讀機制,會有問題。
MySQL預讀,指的是從磁碟載入一個資料頁時,可能會連帶著把這個資料頁相鄰的其他資料頁,也載入到快取裡。比如現在有兩個空閒快取頁,在載入一個資料頁時,就會連帶著把其相鄰的一個資料頁也載入到快取裡去。但是接下來只有一個快取頁被訪問了,另外一個透過預讀機制載入的快取頁,其實並沒被訪問,而此時這兩個快取頁可能都在LRU連結串列前面。
上圖中,前兩個快取頁都是剛載入進來的。但是第二個快取頁是透過預讀機制帶著載入進來的。這個快取頁被放到了連結串列的前面,但實際上沒人訪問。除了第二個快取頁外,第一個快取頁以及最後兩個快取頁一直都有訪問。
這時如果沒有空閒快取頁了,那麼在載入新的資料頁時,就要從LRU連結串列尾部把最近最少使用的一個快取頁拿出來清空騰出空閒。但對於上述情況,這是不合理的,合理的應該是把第二個快取頁清空。
(3)觸發MySQL預讀機制的情況
情況一:引數innodb_read_ahead_threshold預設值是56,意思是如果順序訪問一個區的多個資料頁的數量超過了該閥值。就會觸發預讀機制,把下一個相鄰區中的所有資料頁都載入到快取裡去。
情況二:Buffer Pool裡快取一個區13個連續的會被頻繁訪問的資料頁,此時就會直接觸發預讀機制,把這個區裡的其他資料頁也載入到快取裡。該情況透過引數innodb_random_read_ahead控制,預設OFF表示關閉。
所以,預設情況下第一種情況很可能會觸發預讀機制。並且第一種情況會一下子把相鄰區中很多資料頁載入到快取裡。這些快取頁如果都放在LRU連結串列前面,並且沒什麼訪問了。這樣就會導致一些頻繁被訪問的快取頁放到了LRU連結串列的尾部。最後造成頻繁被訪問的快取頁反而被清空掉。而被清空掉的快取頁很快又要從磁碟中重新載入進入緩衝區。這時不但不合理還很影響效能。
(4)簡單LRU連結串列可能存在的全表掃描問題
全表掃描,就是類似於執行這樣的SQL語句:select * from users。此時沒有加任何一個where條件,這個會導致MySQL把該表所有的資料頁,都從磁碟載入到Buffer Pool裡。
這時LRU連結串列中排在前面的快取頁,可能都是全表掃描載入進來的快取頁。而如果這次全表掃描後,後面幾乎沒有用到這個表裡的資料。那此時LRU連結串列的尾部,也可能都是之前一直被頻繁訪問的快取頁。這樣也會把頻繁訪問的快取頁給淘汰掉,最後留下不經常訪問的全表掃描載入進來的快取頁。
(5)總結
所以如果使用簡單的LRU連結串列機制,其實是漏洞百出的。因為預讀機制、全表掃描會把未來並不經常訪問的資料頁載入到快取頁裡,從而導致那些頻繁被訪問的快取頁不得不處於LRU連結串列尾部。如果此時恰好需要把一些快取頁刷入磁碟或者清空以騰出空閒的快取頁,那麼就會把頻繁被訪問的快取頁給清空了。
簡單LRU連結串列可能存在的問題:
問題一:預讀機制導致相鄰資料頁也一塊被載入到緩衝池。此時在LRU連結串列中排前面的,可能都是透過預讀機制載入進來的。
問題二:全表掃描可能會一下子把一個表的所有資料頁都載入到緩衝池,此時在LRU連結串列中排前面的,可能都是透過全表掃描載入進來的。
觸發預讀機制的情況:
情況一:引數innodb_read_ahead_threshold的預設值是56。如果順序訪問一個區裡多個資料頁,訪問的資料頁的數量超過此閾值。那麼就會觸發預讀機制,將下一個相鄰區中所有資料頁載入到緩衝池。
情況二:如果緩衝池中快取了一個區裡的13個連續的被頻繁訪問的資料頁,那麼就會觸發預讀機制,將這個區裡其他資料頁也載入到緩衝池。這種情況由引數innodb_random_read_ahead控制,預設關閉。
21.基於冷熱資料分離思想設計LRU連結串列
(1)LRU連結串列分為冷資料區域和熱資料區域
(2)冷熱資料分離如何解決預讀和全表掃描問題
(3)LRU連結串列的熱資料區域是如何進行最佳化的
(4)LRU連結串列的冷資料區域中都是些什麼資料
(5)Redis的冷熱資料處理
(1)LRU連結串列分為冷資料區域和熱資料區域
為解決簡單LRU連結串列帶來的預讀和全表掃描問題,InnoDB設計LRU連結串列時用了冷熱資料分離的思想。
InnoDB的LRU連結串列,會被拆分為兩個部分。一部分是熱資料,一部分是冷資料。冷熱資料的比例由innodb_old_blocks_pct引數控制,預設是37。這時候,LRU連結串列看起來如下:
當資料頁第一次被載入到記憶體時,快取頁對應的描述資料塊節點會被放在LRU連結串列的冷資料區域的頭部。被載入到記憶體的資料頁,如果在預設1s後繼續被訪問,則該快取頁對應的描述資料塊節點會被挪動到熱資料區域的連結串列頭部。對應的innodb_old_blocks_time引數預設就是設定為1s。
如果資料載入到快取頁之後過了1s+的時間,該快取頁被訪問,則對應的描述資料塊會被放入熱資料區域的連結串列頭部。如果資料載入到快取頁之後在1s內,該快取頁被訪問,則對應的描述資料塊不會被放入熱資料區域。
(2)冷熱資料分離如何解決預讀和全表掃描問題
這套冷熱資料分離的機制包含三個方案:
方案一:快取頁分冷熱資料載入
方案二:冷資料轉化為熱資料進行時間限制
方案三:淘汰快取頁時優先淘汰冷資料區域
根據這套方案,簡單LRU連結串列遇到的預讀和全表掃描問題都能解決。比如透過預讀機制和全表掃描載入進來的資料頁,大都在1s內訪問一次後就不再訪問了,所以這些資料基本留在冷資料區域。當要淘汰快取頁時,優先選擇冷資料區域尾部的快取頁,這就很合理了。這樣就不會讓剛載入進來的快取頁佔據LRU連結串列的頭部,導致頻繁訪問的快取頁突然在LRU連結串列的尾部而被錯誤淘汰掉。
(3)LRU連結串列的熱資料區域是如何進行最佳化的
如果訪問了熱資料區域中的一個快取頁,是否應該馬上把它移動到熱資料區域的連結串列頭部?由於熱資料區域裡的快取頁可能是被經常訪問的,所以不建議頻繁移動,否則影響效能。
因此LRU連結串列的熱資料區域的訪問規則是:只有在熱資料區域的後3/4部分的快取頁被訪問了,才會移動到連結串列頭部;如果是熱資料區域的前面1/4部分的快取頁被訪問了,那麼不需要移動。這樣儘可能減少連結串列中的節點頻繁移動。
(4)LRU連結串列的冷資料區域中都是些什麼資料
大部分都是預讀載入進來的快取頁,載入進來1s之後沒人訪問。或者全表掃描或者一些大的查詢語句載入一堆資料到快取頁,結果都是1s之內訪問一下,後續就不再訪問的資料。
(5)Redis的冷熱資料處理
透過對key延長過期時間就可以區分出冷熱資料了。具體就是預設key在一定時間過期。熱key每次訪問都延長過期時間,冷key過期就不在Redis裡了。
常見的場景就是電商系統裡的商品快取資料。假設有1億商品,設計快取機制時必須考慮熱資料的快取預載入。比如每天統計出哪些商品被訪問的次數最多,然後系統晚上啟動一個定時任務,把熱門商品資料預載入到Redis裡。
22.Buffer Pool的快取頁以及幾個連結串列總結
Buffer Pool在被使用時,會頻繁從磁碟上載入資料頁到快取頁裡。然後free連結串列、flush連結串列、LRU連結串列都會被同時使用,這三個連結串列都是雙向連結串列。
一.當載入一個資料頁到一個快取頁時
InnoDB就會從free連結串列裡移除這個快取頁。然後會把這個快取頁放入到LRU連結串列的冷資料區域頭部。
二.當修改一個快取頁時
InnoDB就會在flush連結串列中記錄這個髒頁。而且可能會把該快取頁從LRU連結串列的冷資料區域移動到熱資料區域頭部。
三.當查詢一個快取頁時
InnoDB可能會把該快取頁從LRU連結串列冷資料區域移動到熱資料區域頭部,或者從LRU連結串列的熱資料區域其他位置移動到熱資料區域頭部。
總之,MySQL在執行增刪改查時:首先會大量操作快取頁以及對應的幾個連結串列。然後當快取頁滿時,會基於LRU連結串列淘汰快取頁。也就是先把要淘汰的快取頁刷入磁碟,然後清空該快取頁。接著再把需要的資料頁載入到空閒的快取頁中。
23.LRU連結串列冷資料區域的快取頁何時刷盤
(1)LRU連結串列的冷資料區域的快取頁刷盤的幾個時機
(2)如何避免快取頁都用完了(設定Buffer Pool很大的記憶體空間)
(1)LRU連結串列的冷資料區域的快取頁刷盤的幾個時機
時機一:定時把LRU尾部的部分快取頁刷入磁碟
第一個時機並不是在快取頁滿的時候,才會將快取頁刷入磁碟。而是有一個後臺定時任務執行緒,該執行緒會定時把LRU連結串列的冷資料區域尾部的一些快取頁刷入磁碟。然後清空幾個快取頁,並將這些快取頁加回free連結串列。
時機二:把flush連結串列中的一些快取頁定時刷入磁碟
如果僅僅是把LRU連結串列中冷資料區域的快取頁刷入磁碟,還是不夠的。因為在LRU連結串列的熱資料區域裡很多快取頁可能也會被頻繁的修改,這些快取頁不可能永遠都不刷入磁碟中。
所以這個後臺執行緒同時也會在MySQL不怎麼繁忙時,找個時間把flush連結串列中的快取頁都刷入磁碟中。只要flush連結串列中的快取頁被刷入磁碟,則這些快取頁也會從flush連結串列和LRU連結串列中移除,然後加入到free連結串列中。
時機三:實在沒有空閒快取頁時
假設所有的free連結串列都被使用,同時flush連結串列中有很多被修改過的快取頁,以及LRU連結串列中也有很多快取頁進行冷熱資料分離。此時如果要從磁碟載入資料頁到一個空閒快取頁中,就會從LRU連結串列的冷資料區域尾部找到一個快取頁,刷入磁碟和清空。
(2)如何避免快取頁都用完了(設定Buffer Pool很大的記憶體空間)
此時需要先把一個快取頁刷入磁碟騰出空閒快取頁,再從磁碟讀取資料頁。這種情況要執行兩次磁碟IO,效能低下。一次是快取頁刷入磁碟,一次是從磁碟讀取資料頁載入到快取頁。
由於InnoDB在使用快取頁的過程中,會有一個後臺執行緒定時地把LRU連結串列冷資料區域的一些快取頁刷入磁碟。所以快取頁是一邊被使用,一邊被後臺執行緒定時地釋放。如果快取頁被使用得很快,而後臺執行緒釋放快取頁的速度很慢,那麼必然頻繁出現快取頁被使用完的情況。
從InnoDB角度看,它無法控制快取頁被使用的速度。因為快取頁被使用的速度依賴於外部服務呼叫的併發程度。另外InnoDB的後臺執行緒會定時釋放一批快取頁,這個過程也很難最佳化。因為如果頻繁釋放也會造成磁碟IO頻繁,從而影響效能。
所以最後可以依靠InnoDB的Buffer Pool的大小來避免。如果MySQL要抗高併發的訪問,那麼機器必然要配置很大的記憶體空間,起碼是32G+、64GB、128GB。此時就可以設定Buffer Pool很大的記憶體空間,如20GB、48GB,80GB。
這樣在高併發場景下:雖然Buffer Pool的快取頁被頻繁使用,但後臺執行緒也在定時釋放快取頁。由於Buffer Pool記憶體很大,所以可能需要較長時間才會導致快取頁用完。需要的時間越長,那麼就越可以撐到資料庫訪問高峰期已過去。只要高峰一過,後臺執行緒又不停地基於flush連結串列和LRU連結串列釋放快取頁,那麼空閒的快取頁數量又會慢慢多起來。
24.Buffer Pool在訪問時是否需要加鎖
Buffer Pool本質上是一大塊記憶體,由一大堆的快取頁和描述資料塊組成。然後透過free連結串列、flush連結串列、LRU連結串列、雜湊表來輔助執行。
如果MySQL同時接收到多個請求,啟用了多個執行緒來處理這些請求。每個執行緒負責處理一個請求,那麼就會出現多個執行緒併發訪問Buffer Pool。此時這些執行緒都是在訪問著記憶體裡的一些共享資料結構:比如快取頁、各種連結串列,那麼是否必須加鎖?
多執行緒併發訪問一個Buffer Pool,必然是要加鎖的。一個執行緒完成一系列操作後,才能接著下一個執行緒執行操作;比如載入資料頁到快取頁、更新free連結串列、更新LRU連結串列、更新flush連結串列。
加鎖之後資料庫的效能怎樣?
即便在Buffer Pool裡,多個執行緒加鎖序列排隊執行,它的效能也不會太差。因為大部分情況下,每個執行緒都是查詢或更新快取頁裡的資料。這些操作都是發生在記憶體裡,基本都是微秒級(記憶體是百納秒級)。
更新free、flush、LRU這些連結串列,也都是基於指標的操作,效能也很高。但如果執行緒拿到鎖之後,需要從磁碟裡讀取資料頁載入到快取頁中時,由於發生了磁碟IO,那麼耗時就會久一點。
25.多個Buffer Pool最佳化併發能力
MySQL的生產經驗,就是給MySQL設定多個Buffer Pool來最佳化併發能力。如果Buffer Pool的記憶體小於1GB,MySQL預設只會給一個Buffer Pool。如果Buffer Pool的記憶體較大如8G,那麼可給MySQL設定多個Buffer Pool。
下面的配置就給Buffer Pool設定了8GB的記憶體。並且有4個Buffer Pool,每個Buffer Pool的大小就是2GB:
[server]
innodb_buffer_pool_size = 8589934592
innodb_buffer_pool_instances = 4
這樣MySQL執行時就會有4個Buffer Pool,每個Buffer Pool負責管理一部分快取頁和描述資料塊,每個Buffer Pool擁有獨立的free、flush、LR連結串列。
這時即便多個執行緒併發來訪問也可以把壓力分開,比如有的執行緒訪問這個Buffer Pool,有的執行緒訪問另外的Buffer Pool。
透過多個Buffer Pool,MySQL多執行緒併發訪問的效能就會得到提升,多個執行緒可以在不同的Buffer Pool中加鎖和執行自己的操作。
26.透過chunk動態調整執行期的Buffer Pool
MySQL有個chunk機制,這指的是Buffer Pool是由很多個chunk組成的。innodb_buffer_pool_chunk_size引數可以控制chunk的大小,預設是128MB。
假設給MySQL的Buffer Pool設定了總大小是8GB,並且有4個Buffer Pool,那麼每個Buffer Pool就是2GB。此時每個Buffer Pool就是由一系列的128MB的chunk組成的,每個Buffer Pool會有16個chunk,每個Buffer Pool裡的每個chunk裡才是一系列的描述資料塊和快取頁,每個Buffer Pool裡的多個chunk共享一套free、flush、LRU連結串列。
當動態調整Buffer Pool大小時,如Buffer Pool大小由8GB動態加到16GB。那麼此時只要申請一系列的128M大小的chunk,並且每個chunk是連續的128M記憶體即可,然後把這些申請到的chunk記憶體分配到Buffer Pool。這樣就無要額外申請16G的連續記憶體空間,然後把已有的資料進行複製。
所以,InnoDB的Buffer Pool的真實資料結構是:由多個Buffer Pool組成的,每個Buffer Pool由多個chunk組成。
27.生產環境應給Buffer Pool設定多少記憶體、多少Buffer Pool、多大的chunk
通常建議給Buffer Pool設定機器記憶體的50%~60%左右大小。因為作業系統核心也要使用記憶體。
所以32GB記憶體的機器,可以給Buffer Pool設定20GB的記憶體;128GB記憶體的機器,可以給Buffer Pool設定80GB的記憶體。
確定Buffer Pool總大小後,就需要設定多少個Buffer Pool以及chunk大小。Buffer Pool總大小 = (chunk大小 * chunk數量) * Buffer Pool數量
預設chunk的大小是128MB。如果機器的記憶體是32GB,設定了Buffer Pool總大小是20GB。那麼Buffer Pool數量可以設定為10個,每個Buffer Pool由16個chunk組成。因為此時一個Buffer Pool = chunk大小 * chunk數量 = 128MB * 16 = 2GB。10個Buffer Pool就是總大小20GB。
當然也可設定Buffer Pool數量為16個,每個Buffer Pool由10個chunk組成。此時一個Buffer Pool = chunk大小 * chunk數量 = 1280MB。16個Buffer Pool也是總大小20GB。
當然也可設定Buffer Pool數量為32個,每個Buffer Pool由5個chunk組成。此時一個Buffer Pool = chunk大小 * chunk數量 = 640MB。32個Buffer Pool也是總大小20GB;
28.show engine innodb status輸出詳解
> show engine innodb status ;
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 274857984 //意思是Buffer Pool最終的總大小是多少
Dictionary memory allocated 116177
Buffer pool size 16382 //意思是Buffer Pool一共能容納多少個快取頁
Free buffers 16002 //意思是free連結串列中一共有多少個空閒的快取頁是可用的
Database pages 380 //意思是lru連結串列中一共有多少個快取頁
Old database pages 0 //冷資料區域裡的快取頁數量
Modified db pages 0 //flush連結串列中的快取頁數量
Pending reads 0 //等待從磁碟上載入進快取頁的數量
Pending writes: LRU 0, flush list 0, single page 0 //即將從lru連結串列中刷入磁碟的數量,即將從flush連結串列中刷入磁碟的數量
Pages made young 0, not young 0 //在lru冷資料區域裡訪問之後轉移到熱資料區域的快取頁數量,在lru冷資料區域1s內被訪問了沒進入熱資料區域的快取頁數量
0.00 youngs/s, 0.00 non-youngs/s //每秒從冷資料區域進入熱資料區域的快取頁數量,每秒在冷資料區域裡被訪問了但不能進入熱資料區域的快取頁數量
Pages read 345, created 35, written 37 //已經讀取、建立和寫入了多少個快取頁
0.00 reads/s, 0.00 creates/s, 0.00 writes/s //每秒讀取、建立和寫入的快取頁數量
No buffer pool page gets since the last printout
Buffer pool hit rate xxx / 1000, //每1000次訪問有多少次直接命中Buffer Pool裡的快取
young-making rate xxx / 1000 not xx / 1000 //每1000次訪問有多少次訪問讓快取頁從冷資料區域移動到熱資料區域以及沒移動的快取頁數量
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 380, unzip_LRU len: 0 //lru連結串列裡快取頁的數量
I/O sum[0]:cur[0], unzip sum[0]:cur[0] //最近50s讀取磁碟頁的總數和正在讀取磁碟頁的總數