Buffer Pool 為了讓 MySQL 變快都做了什麼?
前言
大家好,我是阿星,又跟大家見面了。
相信很多小夥伴在面試中都被問過「為什麼要用快取?」,大部分人都是回答:「減少資料庫的磁碟IO
壓力」。
但是MySQL
真的有如此不堪嗎?
每次增刪改查都要去走磁碟IO
嗎?
今天就聊聊InnoDB
對Buffer Pool
的奇思妙想。
Buffer Pool
先梳理出問題,再思考如何解決問題。
假設我們就是InnoDB
,我們要如何去解決磁碟IO
問題?
這個簡單,做快取就好了,所以MySQL
需要申請一塊記憶體空間,這塊記憶體空間稱為Buffer Pool
。
Buffer Pool
是申請下來了,但是Buffer Pool
裡面放什麼,要怎麼規劃?
快取頁
MySQL
資料是以頁為單位,每頁預設16KB
,稱為資料頁,在Buffer Pool
裡面會劃分出若干個快取頁與資料頁對應。
感覺還少了點什麼,我們如何知道快取頁對應那個資料頁呢?
描述資料
所有還需要快取頁的元資料資訊,可以稱為描述資料,它與快取頁一一對應,包含一些所屬表空間、資料頁的編號、Buffer Pool
中的地址等等。
後續對資料的增刪改查都是在Buffer Pool
- 查詢:從磁碟載入到快取,後續直接查快取
- 插入:直接寫入快取
- 更新刪除:快取中存在直接更新,不存在載入資料頁到快取更新
可能有小夥伴擔心,MySQL
宕機了,資料不就全丟了嗎?
這個不用擔心,因為InnoDB
提供了WAL
技術(Write-Ahead Logging),通過redo log
讓MySQL
擁有了崩潰恢復能力。
再配合空閒時,會有非同步執行緒做快取頁刷盤,保證資料的永續性與完整性。
如果不能理解redo log
是如何恢復資料的,可以看看阿星前面兩篇文章
- 02.淺談 MySQL InnoDB 的記憶體元件
- 03.聊聊redo log是什麼?
另外,直接更新資料的快取頁稱為髒頁,快取頁刷盤後稱為乾淨頁
Free連結串列
MySQL
資料庫啟動時,按照設定的Buffer Pool
大小,去找作業系統申請一塊記憶體區域,作為Buffer Pool
(假設申請了512MB)。
申請完畢後,會按照預設快取頁的16KB
以及對應的800Byte
的描述資料,在Buffer Pool
中劃分出來一個一個的快取頁和它們對應的描述資料。
MySQL
執行起來後,會不停的執行增刪改查,需要從磁碟讀取一個一個的資料頁放入Buffer Pool
對應的快取頁裡,把資料快取起來,以後就可以在記憶體裡執行增刪改查。
但是這個過程必然涉及一個問題,哪些快取頁是空閒的?
為了解決這個問題,我們使用連結串列結構,把空閒快取頁的描述資料放入連結串列中,這個連結串列稱為free
連結串列。
針對free
連結串列我們要做如下設計
- 新增
free
基礎節點 - 描述資料新增
free
節點指標
最終呈現出來的,是由空閒快取頁的描述資料組成的free
連結串列。
有了free
連結串列之後,我們只需要從free
連結串列獲取一個描述資料,就可以獲取到對應的快取頁。
往描述資料與快取頁寫入資料後,就將該描述資料移出free
連結串列。
快取頁雜湊表
資料頁是快取進去了,但是又一個問題來了。
下次查詢資料時,如何在Buffer Pool
裡快速定位到對應的快取頁呢?
難道需要一個非空閒的描述資料連結串列,再通過表空間號+資料頁編號遍歷查詢嗎?
這樣做也可以實現,但是效率不太高,時間複雜度是O(N)
。
所以我們可以換一個QQ交易地圖結構,使用雜湊表來快取它們間的對映關係,時間複雜度是O(1)
。
表空間號+資料頁號,作為一個key
,然後快取頁的地址作為value
。
每次載入資料頁到空閒快取頁時,就寫入一條對映關係到快取頁雜湊表中。
後續的查詢,就可以通過快取頁雜湊表路由定位了。
Flush連結串列
還記得之前有說過「空閒時會有非同步執行緒做快取頁刷盤,保證資料的永續性與完整性」嗎?
新問題來了,難道每次把Buffer Pool
裡所有的快取頁都刷入磁碟嗎?
當然不能這樣做,磁碟IO
開銷太大了,應該把髒頁刷入磁碟才對(更新過的快取頁)。
可是我們怎麼知道,那些快取頁是髒頁?
很簡單,參照free
連結串列,弄個flush
連結串列出來就好了,只要快取頁被更新,就將它的描述資料加入flush
連結串列。
針對flush
連結串列我們要做如下設計
- 新增
flush
基礎節點 - 描述資料新增
flush
節點指標
最終呈現出來的,是由更新過資料的快取頁描述資料組成的flush
連結串列。
後續非同步執行緒都從flush
連結串列刷快取頁,當Buffer Pool
記憶體不足時,也會優先刷flush
連結串列裡的快取頁。
LRU連結串列
目前看來Buffer Pool
的功能已經比較完善了。
但是仔細思考下,發現還有一個問題沒處理。
MySQL
資料庫隨著系統的執行會不停的把磁碟上的資料頁載入到空閒的快取頁裡去,因此free
連結串列中的空閒快取頁會越來越少,直到沒有,最後磁碟的資料頁無法載入。
為了解決這個問題,我們需要淘汰快取頁,騰出空閒快取頁。
可是我們要優先淘汰那些快取頁?總不能一股腦直接全部淘汰吧?
這裡就要借鑑LRU
演算法思想,把最少使用的快取頁淘汰(命中率低),提供LRU
連結串列出來。
針對LRU
連結串列我們要做如下設計
- 新增
LRU
基礎節點 - 描述資料新增
LRU
節點指標
實現思路也很簡單,只要是查詢或修改過快取頁,就把該快取頁的描述資料放入連結串列頭部,也就說近期訪問的資料一定在連結串列頭部。
當free
連結串列為空的時候,直接淘汰LRU
連結串列尾部快取頁即可。
LRU連結串列優化
麻雀雖小五臟俱全,基本Buffer Pool
裡與快取頁相關的元件齊全了。
但是快取頁淘汰這裡還有點問題,如果僅僅只是使用LRU
連結串列的機制,有兩個場景會讓熱點資料被淘汰。
- 預讀機制
- 全表掃描
預讀機制是指MySQL
載入資料頁時,可能會把它相鄰的資料頁一併載入進來(區域性性原理)。
這樣會帶來一個問題,預讀進來的資料頁,其實我們沒有訪問,但是它卻排在前面。
正常來說,淘汰快取頁時,應該把這個預讀的淘汰,結果卻把尾部的淘汰了,這是不合理的。
我們接著來看第二個場景全表掃描,如果表資料量大,大量的資料頁會把空閒快取頁用完。
最終LRU
連結串列前面都是全表掃描的資料,之前頻繁訪問的熱點資料全部到隊尾了,淘汰快取頁時就把熱點資料頁給淘汰了。
為了解決上述的問題。
我們需要給LRU
連結串列做冷熱資料分離設計,把LRU
連結串列按一定比例,分為冷熱區域,熱區域稱為young
區域,冷區域稱為old
區域。
以7:3為例,young區域70%,old`區域30%
如上圖所示,資料頁第一次載入進快取頁的時候,是先放入冷資料區域的頭部,如果1秒後再次訪問快取頁,則會移動到熱區域的頭部。
這樣就保證了預讀機制與全表掃描載入的資料都在連結串列隊尾。
young
區域其實還可以做一個小優化,為了防止young
區域節點頻繁移動到表頭。
young
區域前面1/4
被訪問不會移動到連結串列頭部,只有後面的3/4
被訪問了才會。
記住是按照某個比例將
LRU
連結串列分成兩部分,不是某些節點固定是young
區域的,某些節點固定是old
區域的,隨著程式的執行,某個節點所屬的區域也可能發生變化。
小結
其實MySQL
就是這樣實現Buffer Pool
快取頁的,只不過它裡面的連結串列全是雙向連結串列,阿星這裡偷個懶,但是不影響理解思路。
讀到這裡,我相信大家對Buffer Pool
快取頁有了深刻的認知,也知道從一個增刪改查開始,如何快取資料、定位快取、快取刷盤、快取淘汰。
這裡留問題給大家思考,Free、Flush、LRU
這三個連結串列之間的聯絡,隨著MySQL
一直在執行,它們會產生怎樣的聯動。