MySQL 頁完全指南——淺入深出頁的原理

detectiveHLH發表於2021-06-21

之前寫了一些關於 MySQL 的 InnoDB 儲存引擎的文章,裡面好幾次都提到了(Pages)這個概念,但是都只是簡要的提了一下。例如之前在聊 InnoDB記憶體結構 時提到過,但當時的重點是記憶體架構,就沒有展開深入。

我發現有好幾次都需要提到頁,那我就正好拿一篇來詳細的講講 InnoDB 中的頁。

頁是什麼

首先,我們需要知道,頁(Pages)是 InnoDB 中管理資料的最小單元。Buffer Pool 中存的就是一頁一頁的資料。再比如,當我們要查詢的資料不在 Buffer Pool 中時,InnoDB 會將記錄所在的頁整個載入到 Buffer Pool 中去;同樣的,將 Buffer Pool 中的髒頁刷入磁碟時,也是按照頁為單位刷入磁碟的。

不瞭解 Buffer Pool 的、或者感興趣的可以去文章開頭給的連結熟悉一下

頁的概覽

我們往 MySQL 插入的資料最終都是存在頁中的。在 InnoDB 中的設計中,頁與頁之間是通過一個雙向連結串列連線起來。

< src src="https://tva1.sinaimg.cn/large/008i3skNgy1griuw8hywkj31500ic757.jpg" alt style="margin: 0 auto; max-width: 100%; width: 100%; border-radius: 5px; display: block; margin-bottom: 15px; height: auto;">

而儲存在頁中的一行一行的資料則是通過單連結串列連線起來的。

上圖中的 User Records 的區域就是用來儲存行資料的。那 InnoDB 為什麼要這麼設計?假設我們沒有頁這個概念,那麼當我們查詢時,成千上萬的資料要如何做到快速的查詢出結果?眾所周知,MySQL 的效能是不錯的,而如果沒有頁,我們剩下的只能是逐條逐條的遍歷資料了。

那頁是如何做到快速查詢的呢?在當前頁中,可以通過 User Records 中的連線每條記錄的單連結串列來進行遍歷,如果在當前頁中沒有找到,則可以通過下一頁指標快速的跳到下一頁進行查詢。

Infimum 和 Supremum

有人可能會說了,你在 User Records 中還不是通過遍歷來解決的,你就是簡單的把資料分了個組而已。如果我的資料根本不在當前這個頁中,那我難道還是得把之前的頁中的每一條資料全部遍歷完?這效率也太低了

當然,MySQL 也考慮到了這個問題,所以實際上在頁中還存在一塊區域叫做 The Infimum and Supremum Records ,代表了當前頁中最大最小的記錄。

有了 Infimum RecordSupremum Record ,現在查詢不需要將某一頁的 User Records 全部遍歷完,只需要將這兩個記錄和待查詢的目標記錄進行比較。比如我要查詢的資料 id = 101 ,那很明顯不在當前頁。接下來就可以通過下一頁指標跳到下頁進行檢索。

使用Page Directory

可能有人又會說了,你這 User Records 裡不也全是單連結串列嗎?即使我知道我要找的資料在當前頁,那最壞的情況下,不還是得挨個挨個的遍歷100次才能找到我要找的資料?你管這也叫效率高?

不得不說,這的確是個問題,不過是一個 MySQL 已經考慮到的問題。不錯,挨個遍歷確實效率很低。為了解決這個問題,MySQL 又在頁中加入了另一個區域 Page Directory

顧名思義,Page Directory 是個目錄,裡面有很多個槽位(Slots),每一個槽位都指向了一條 User Records 中的記錄。大家可以看到,每隔幾條資料,就會建立一個槽位。其實我圖中給出的資料是非常嚴格按照其設定來的,在一個完整的頁中,每隔6條資料就會有一個 Slot。

Page Directory 的設計不知道有沒有讓你想起另一個資料結構——跳錶,只不過這裡只抽象了一層索引

MySQL 會在新增資料的時候就將對應的 Slot 建立好,有了 Page Directory ,就可以對一張頁的資料進行粗略的二分查詢。至於為什麼是粗略,畢竟 Page Directory 中不是完整的資料,二分查詢出來的結果只能是個大概的位置,找到了這個大概的位置之後,還需要回到 User Records 中繼續的進行挨個遍歷匹配。

不過這樣的效率已經比我們剛開始聊的原始版本高了很多了。

頁的真實面貌

如果我開篇就把頁的各種組成部分,各種概念直接丟擲來,首先我自己接受不了,這樣顯得很僵硬。其次,對頁不熟悉的人應該是不太能理解頁為什麼要這麼設計的。所以我按照查詢一條資料的一套思路,把頁的大致的面貌呈現給了大家。

實際上,頁上還儲存了很多其他的欄位,也還有其他的區域,但是這些都不會影響到我們對頁的理解。所以,在對頁有了一個較為清晰的認知之後,我們就可以來看看真實的頁到底長啥樣了。

上圖就是頁的實際全部組成,除了我們之前提到過的,還多了一些之前沒有聊過的,例如 File HeaderPage HeaderFree SpaceFile Tailer 。我們一個一個來看。

File Header

其實File Header 在上文已經聊過了,只是不叫這個名字。上面提到的上一頁指標下一頁指標其實就是屬於File Header的,除此之外還有很多其他的資料。

其實我比較抗拒把一堆引數列出來,告訴你這個大小多少,那個用來幹嘛。對於我們需要詳細瞭解頁來說,其實暫時只需要知道兩個就足夠了,分別是:

  • FIL_PAGE_PREV
  • FIL_PAGE_NEXT

這兩個變數就是上文提到過的上一頁指標下一頁指標,說是指標,是為了方便大家理解,實際上是頁在磁碟上的偏移量。

Page Header

比起 File HeaderPage Header 中的資料對我們來說就顯得更加熟悉了,我這裡畫了一張圖,把裡面的內容詳細的列了出來。

這裡全列出來是因為了解這些引數的含義和為什麼要設定引數,能夠更好的幫助我們瞭解頁的原理和構造,具體的看圖說話就行。

這裡也很想吐槽,太多部落格都寫的太僵硬,比如引數 PAGE_HEAP_TOP ,這裡的 HEAP 很多部落格都直接叫。這就跟你給Init寫註釋叫初始化一樣,還不如不寫。實際上你去研究一下就會知道,這裡的堆實際上就是指User Records

裡面有個兩個引數可能會有點混淆,分別是PAGE_N_HEAPPAGE_N_RECS ,都是當前 User Records 中記錄的數量,唯一的不同在於,PAGE_N_HEAP 中是包含了被標記為刪除的記錄的, 而 PAGE_N_RECS 中就是實際上我們能夠查詢到的所有資料。

Infimum & Supremum Records

上文中提到,Infimum & Supremum Records會記錄當前頁最大最小記錄。實際上不準確,更準確的描述是最小記錄和最大紀錄的開區間。因為實際上 Infimum Records 會比當前頁中的最小值還要小,而 Supremum Records 會比當前頁中的最大值要大。

User Records

User Records 可以說是我們平時接觸的最多的部分了,畢竟我們的資料最終都在這。頁被初始化之後,User Records 中是沒有資料的,隨著系統執行,資料產生,User Records 中的資料會不斷的膨脹,相應的 Free Space 空間會慢慢的變小。

關於 User Records 中的概念,之前已經聊過了。這裡只聊我認為很關鍵的一點,那就是順序

我們知道,在聚簇索引中,Key 實際上會按照 Primary Key 的順序來進行排列。那在 User Records 中也會這樣嗎?我們插入一條新的資料到 User Records 中時,是否也會按照 Primary Key 的順序來對已有的資料重排序?

答案是不會,因為這樣會拉低 MySQL 處理的效率。

User Records 中的資料是由單連結串列指標的指向來保證的,也就是說,行資料實際在磁碟上的表現,是按照插入順序來排隊的,先到的資料在前面,後來的資料在後面。只不過通過 User Records 中的行資料之間的單連結串列形成了一個按照 Primary Key排列的順序。

用圖來表示,大概如下:

Free Space

這塊其實變相的在其他的模組中討論了,最初 User Records 是完全空的,當有新資料進來時,會來 Free Space 中申請空間,當 Free Space 沒空間了,則說明需要申請新的頁了,其他沒什麼特別之處。

Page Directory

這跟上文討論的沒什麼出入,就直接跳過了。

File Trailer

這塊主要是為了防止頁在刷入磁碟的過程中,由於極端的意外情況(網路問題、火災、自然災害)導致失敗,而造成資料不一致的情況,也就是說形成了髒頁。

裡面有隻有一個組成部分:

File Trailer
File Trailer

總結

到此,我認為關於的所有東西就聊的差不多了,瞭解了底層的頁原理,我個人認為是有助於我們更加友好、理智的使用 MySQL 的,使其能發揮出自己應該發揮的極致效能。

好了以上就是本篇部落格的全部內容了,歡迎微信搜尋關注【SH的全棧筆記】,回覆【佇列】獲取MQ學習資料,包含基礎概念解析和RocketMQ詳細的原始碼解析,持續更新中。

如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章