innodb是如何存資料的?yyds

蘇三說技術發表於2021-08-23

前言

如果你使用過mysql資料庫,對它的儲存引擎:innodb,一定不會感到陌生。

眾所周知,在mysql8以前,預設的儲存引擎是:myslam。但mysql8之後,預設的儲存引擎已經變成了:innodb,它是我們建表的首選儲存引擎。

那麼,問題來了:

  1. innodb的底層是如何儲存資料的?
  2. 表中有哪些隱藏列?
  3. 使用者記錄之間是如何關聯起來的?

如果你想知道上面三個問題的答案,那麼,請繼續往下面看。

本文主要包含如下內容:

1.磁碟or記憶體?

1.1 磁碟

資料對系統來說是非常重要的東西,比如:使用者的身份證、手機號、銀行號、會員過期時間、積分等等。一旦丟失,會對使用者造成很大的影響。

那麼問題來了,如何才能保證這些重要的資料不丟呢?

答案:把資料存在磁碟上。

當然有人會說,如果磁碟壞了怎麼辦?

那就需要備份,或者做主從了。。。

好了,打住,這不是今天的重點。

言歸正傳。

大家都知道,從磁碟上讀寫資料,至少需要兩次IO請求才能完成。一次是讀IO,另一次是寫IO。

而IO請求是比較耗時的操作,如果頻繁的進行IO請求勢必會影響資料庫的效能。

那麼,如何才能解決資料庫的效能問題呢?

1.2 記憶體

把資料存在暫存器?

沒錯,作業系統從暫存器中讀取資料是最快的,因為它離CPU最近。

但是暫存器有個非常致命的問題是:它只能儲存非常少量的資料,設計它的目的主要是用來暫存指令和地址,並非儲存大量使用者資料的。

這樣看來,只能把資料存在記憶體中了。

因為記憶體同樣能滿足我們,快速讀取和寫入資料的需求,而且效能是非常可觀的,只是比較暫存器稍稍慢了一丟丟而已。

不過有個讓人討厭的地方是,記憶體相對於磁碟來說,是更加昂貴的資源。通常情況下,500G或者1T的磁碟,是很常見的。但你有聽說過有500G的記憶體嗎?別人會以為你瘋了。記憶體大小討論的數量級一般是16G或32G。

記憶體可以儲存一些使用者資料,但無法儲存所有的使用者資料,因為如果資料量太大了,它可能還是存不下。

此外,即使使用者資料能剛好存在記憶體,以後萬一有一天,資料庫伺服器或者部署節點掛了,或者重啟了,資料不就丟了?

怎麼做,才能不會因為異常情況,而丟資料。同時,又能保證資料的讀寫速度呢?

2.資料頁

我們可以把一批資料放在一起。

寫操作時,先將資料寫到記憶體的某個批次中,然後再將該批次的資料一次性刷到磁碟上。如下圖所示:

讀操作時,從磁碟上一次讀一批資料,然後載入到記憶體當中,以後就在記憶體中操作。如下圖所示:

將記憶體中的資料刷到磁碟,或者將磁碟中的資料載入到記憶體,都是以批次為單位,這個批次就是我們常說的:資料頁

當然innodb中存在多種不同型別的頁,資料頁只是其中一種,我們在這裡重點介紹一下資料頁。

那麼問題來了,什麼是資料頁?

資料頁主要是用來儲存表中記錄的,它在磁碟中是用雙向連結串列相連的,方便查詢,能夠非常快速得從一個資料頁,定位到另一個資料頁。

很多時候,由於我們表中的資料比較多,在磁碟中可能存放在多個資料頁當中。

有一天,我們要根據某個條件查詢資料時,需要從一個資料頁找到另一個資料頁,這時候的雙向連結串列就派上大用場了。磁碟中各資料頁的整體結構如下圖所示:

通常情況下,單個資料頁預設的大小是16kb。當然,我們也可以通過引數:innodb_page_size,來重新設定大小。不過,一般情況下,用它的預設值就夠了。

好吧,資料頁的整體結構已經搞明白了。

那麼,單個資料頁包含哪些內容呢?


從上圖中可以看出,資料頁主要包含如下幾個部分:

  • 檔案頭部
  • 頁頭部
  • 最大和最小記錄
  • 使用者記錄
  • 空閒空間
  • 頁目錄
  • 檔案尾部

3.使用者記錄

對於新申請的資料頁,使用者記錄是空的。當插入資料時,innodb會將一部分空閒空間分配給使用者記錄。

使用者記錄是innodb的重中之重,我們平時儲存到資料庫中的資料,就儲存在它裡面。那麼,它裡面又包含哪些內容呢?你不好奇嗎?

其實在innodb支援的資料行格式有四種:

  1. compact行格式
  2. redundant行格式
  3. dynamic行格式
  4. compressed行格式

我們以compact行格式為例:

一條使用者記錄主要包含三部分內容:

  1. 記錄額外資訊,它包含了變長欄位、null值列表和記錄頭資訊。
  2. 隱藏列,它包含了行id、事務id和回滾點。
  3. 真正的資料列,包含真正的使用者資料,可以有很多列。

下面讓我們一起了解一下這些內容。

3.1 額外資訊

額外資訊並非真正的使用者資料,它是為了輔助存資料用的。

3.1.1 變長欄位列表

有些資料如果直接存會有問題,比如:如果某個欄位是varchar或text型別,它的長度不固定,可以根據存入資料的長度不同,而隨之變化。

如果不在一個地方記錄資料真正的長度,innodb很可能不知道要分配多少空間。假如都按某個固定長度分配空間,但實際資料又沒佔多少空間,豈不是會浪費?

所以,需要在變長欄位中記錄某個變長欄位佔用的位元組數,方便按需分配空間。

3.1.2 null值列表

資料庫中有些欄位的值允許為null,如果把每個欄位的null值,都儲存到使用者記錄中,顯然有些浪費儲存空間。

有沒有辦法只簡單的標記一下,不儲存實際的null值呢?

答案:將為null的欄位儲存到null值列表。

在列表中用二進位制的值1,表示該欄位允許為null,用0表示不允許為null。它只佔用了1位,就能表示某個字元是否為null,確實可以節省很多儲存空間。

3.1.3 記錄頭資訊

記錄頭資訊用於描述一些特殊的屬性。


它主要包含:

  • deleted_flag: 即刪除標記,用於標記該記錄是否被刪除了。
  • min_rec_flag: 即最小目錄標記,它是非葉子節點中的最小目錄標記。
  • n_owned:即擁有的記錄數,記錄該組索引記錄的條數。
  • heap_no:即堆上的位置,它表示當前記錄在堆上的位置。
  • record_type:即記錄型別,其中:0表示普通記錄,1表示非葉子節點,2表示Infrimum記錄, 3表示Supremum記錄。
  • next_record:即下一條記錄的位置。

3.2 隱藏列

資料庫在儲存一條使用者記錄時,會自動建立一些隱藏列。如下圖所示:
目前innodb自動建立的隱藏列有三種:

  • db_row_id,即行id,它是一條記錄的唯一標識。
  • db_trx_id,即事務id,它是事務的唯一標識。
  • db_roll_ptr,即回滾點,它用於事務回滾。

如果表中有主鍵,則用主鍵做行id,無需額外建立。如果表中沒有主鍵,假如有不為null的unique唯一鍵,則用它做為行id,同樣無需額外建立。

如果表中既沒有主鍵,又沒有唯一鍵,則資料庫會自動建立行id。

也就是說在innodb中,隱藏列中事務id回滾點是一定會被建立的,但行id要根據實際情況決定。

3.3 真正資料列

真正的資料列中儲存了使用者的真實資料,它可以包含很多列的資料。這個比較簡單,沒有什麼好多說的。

3.4 使用者記錄是如何相連的?

通過上面介紹的內容,大家對一條使用者記錄是如何儲存的,應該有了一定的認識。

但問題來了,一條使用者記錄和另一條使用者記錄是如何相連的,innodb是怎麼知道,某條記錄的下一條記錄是誰?

答案是:用前面提到過的, 記錄額外資訊 》 記錄頭資訊 》下一條記錄的位置。


多條使用者記錄之間通過下一條記錄的位置,組成了一個單向連結串列。這樣就能從前往後,找到所有的記錄了。

4.最大和最小記錄

從上面可以得知,在一個資料頁當中,如果存在多條使用者記錄,它們是通過下一條記錄的位置相連的。

不過有個問題:如果才能快速找到最大的記錄和最小的記錄呢?

這就需要在儲存使用者記錄的同時,也儲存最大和最小記錄了。

最大記錄儲存到Supremum記錄中。

最小記錄儲存在Infimum記錄中。

在儲存使用者記錄時,資料庫會自動建立兩條額外的記錄:Supremum 和 Infimum。它們之間的關係,如下圖所示:


從圖中可以看出使用者資料是從最小記錄開始,通過下一條記錄的位置,從小到大,一步步查詢,最後找到最大記錄為止。

5.頁目錄

從上面可以看出,如果我們要查詢某條記錄的話,資料庫會從最小記錄開始,一條條查詢所有記錄。如果中途找到了,則直接返回該記錄。如果一直找到最大記錄,還沒有找到想要的記錄,則返回空。

咋一看,沒有問題。

但如果仔細想想。

效率會不會有點低?

這不是要對整頁使用者資料進行掃描嗎?

有沒有更高效的方法?

這就需要使用頁目錄了。

說白了,就是把一頁使用者記錄分為若干組,每一組的最大記錄都儲存到一個地方,這個地方就是頁目錄。每一組的最大記錄叫做

由此可見,頁目錄是有多個槽組成的。所下圖所示:


假設一頁的資料分為4組,這樣在頁目錄中,就對應了4個槽,每個槽中都儲存了該組資料的最大值。

這樣就能通過二分查詢,比較槽中的記錄跟需要找到的記錄的大小。如果使用者需要查詢的記錄,小於當前槽中的記錄,則向上查詢上一個槽。如果使用者需要查詢的記錄,大於當前槽中的記錄,則向下查詢下一個槽。

如此一來,就能通過二分查詢,快速的定位需要查詢的記錄了。

so easy

6.檔案頭部和尾部

6.1 檔案頭部

通過前面介紹的行記錄中下一條記錄的位置頁目錄,innodb能非常快速的定位某一條記錄。但有個前提條件,就是使用者記錄必須在同一個資料頁當中。

如果使用者記錄非常多,在第一個資料頁找不到我們想要的資料,需要到另外一頁找該怎麼辦呢?

這時就需要使用檔案頭部了。

它裡面包含了多個資訊,但我只列出了其中4個最關鍵的資訊:

  1. 頁號
  2. 上一頁頁號
  3. 下一頁頁號
  4. 頁型別

顧名思義,innodb是通過頁號、上一頁頁號和下一頁頁號來串聯不同資料頁的。如下圖所示:

不同的資料頁之間,通過上一頁頁號和下一頁頁號構成了雙向連結串列。這樣就能從前向後,一頁頁查詢所有的資料了。

此外,頁型別也是一個非常重要的欄位,它包含了多種型別,其中比較出名的有:資料頁、索引頁(目錄項頁)、溢位頁、undo日誌頁等。

6.2 檔案尾部

我之前提過,資料庫的資料是以資料頁為單位,載入到記憶體中,如果資料有更新的話,需要重新整理到磁碟上。

但如果某一天比較倒黴,程式在重新整理到磁碟的過程中,出現了異常,比如:程式被kill掉了,或者伺服器被重啟了。

這時候資料可能只重新整理了一部分,如何判斷上次刷盤的資料是完整的呢?

這就需要用到檔案尾部

它裡面記錄了頁面的校驗和

在資料重新整理到磁碟之前,會先計算一個頁面的校驗和。後面如果資料有更新的話,會計算一個新值。檔案頭部中也會記錄這個校驗和,由於檔案頭部在前面,會先被重新整理到磁碟上。

接下來,重新整理使用者記錄到磁碟的時候,假設重新整理了一部分,恰好程式出現異常了。這時,檔案尾部的校驗和,還是一箇舊值。資料庫會去校驗,檔案尾部的校驗和,不等於檔案頭部的新值,說明該資料頁的資料是不完整的。

7.頁頭部

通過上面介紹的內容,資料頁之間能夠輕鬆訪問了,但剩下還有個比較重要的問題,就是記錄的狀態資訊。

比如一頁資料到底儲存了多條記錄,或者頁目錄到底使用了多個槽等。這些資訊是實時統計,還是事先統計好了,儲存到某個地方?

為了效能考慮,上面的這些統計資料,當然是先統計好,儲存到一個地方。後面需要用到該資料時,再讀取出來會更好。這個儲存統計資料的地方,就是頁頭部

當然頁頭部不僅僅只儲存:槽的數量、記錄條數等資訊。

它還記錄了:

  • 已刪除記錄所佔的位元組數
  • 最後插入記錄的位置
  • 最大事務id
  • 索引id
  • 索引層級

其實還有很多,在這裡就不一一列舉了,有興趣的朋友可以找我私聊。

總結

多個資料頁之間通過頁號構成了雙向連結串列。而每一個資料頁的行資料之間,又通過下一條記錄的位置構成了單項鍊表。整體架構圖如下:


好了,本文內容先到這裡。如果小夥伴們有任何疑問的話,歡迎找我私聊。

順便預告一下,在innodb的儲存結構中,還有一個非常重要的內容沒講,它就是:索引。敬請期待,我們下期見。

最後說一句(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

關注公眾號:【蘇三說技術】,在公眾號中回覆:面試、程式碼神器、開發手冊、時間管理有超讚的粉絲福利,另外回覆:加群,可以跟很多BAT大廠的前輩交流和學習。

相關文章