InnoDB 表空間

這應該是 MySQL 原理中最底層的部分了,我們存在 MySQL 中的數據,到底在磁盤上長啥樣。你可能會說,數據不都存儲在聚簇索引中嗎?但很遺憾,你並沒有回答我的問題。我會再問你,那聚簇索引在磁盤上又長啥樣?

就像 Redis 的 RDB 文件,最終落在磁盤上就是一個真真切切的 dump.rdb 文件,而 MySQL 就顯得有點迷,我們只知道通過 SQL 去拿數據,並不知道數據最終是以什麼方式進行存儲的。當然,了解其底層的存儲邏輯,並不僅僅是為了滿足好奇心這麼簡單。

其底層的存儲方式,會影響到聚簇索引中數據的存儲,進而影響到 MySQL 的 DML(Data Manipulation Language) 性能,所以對底層存儲邏輯有個清晰的認知,就能夠在某些對性能有着極致追求的場景下,幫助我們對 MySQL 進行優化。

表在磁盤上到底長啥樣

首先我們先不扯像表空間這類的專業詞彙,讓我們先來建一張表,從磁盤的結構上來看一下。首先你得找到 MySQL 的數據目錄,如果你是用 Docker 啟動的話,這個目錄大概長下面這樣:

/data00/docker/volumes/ef876f70d5f5c95325c2a79689db79cc4d1cecb7d96e98901256bd49ca359287/_data

然後我們新建一個叫 test 的 DB,然後在 _data 的這個目錄下就會多一個 test 的目錄。然後在 test 數據庫下新建了一張 student 的表,在 test 目錄下就會多出兩個文件,分別是 student.frmstudent.ibd

可以發現,最終數據在磁盤上的宏觀表現其實很簡單,就這麼些個文件,什麼索引啊、頁啊都先忽略不管。

對於後綴為 .frm 文件,裏面都有啥?裏面包含了每張表的元數據,包括了表的結構定義。而 .ibd 文件里則存放了該表的數據索引

我看到有人在博客里把 .ibd 寫成了 .idb…雖然 db 看着更順,但很遺憾並不正確,你把 ibd 的全稱 innodb data 記住,就不會把縮寫記錯了。

上面說的這個以表名命名的 ibd 文件,其實還有一個專業術語叫表空間

顧名思義可以理解為我這個專屬的空間

認識表空間

如果我上來就直接告訴你,InnoDB 中有個概念叫表空間,你大概率是很難理解的。

像上文描述的這種每張表都有自己單獨的數據存儲文件的,叫獨佔表空間;相對應的,InnoDB 還有自己的系統表空間,在系統表空間下,所有表的數據都存儲在同一個文件中。

那數據什麼時候存儲在系統表空間,又什麼時候存儲在獨佔表空間呢?

這個可以通過 MySQL 的配置項 innodb_file_per_table 來決定。當該配置項開啟時,每張表都會有自己單獨的表空間;相反,當該配置項關閉時,表數據將會存儲在系統的表空間內。

該配置項是默認開啟的,你可以在 MySQL 中通過命令 SHOW VARIABLES LIKE 'innodb_file_per_table' 來查看該變量的狀態

其實從 MySQL 將獨佔表空間作為默認的設置來看,你就應該知道獨佔表空間的性能肯定是要比系統表空間好的。

因為對於系統表空間來說,通常只有一個文件,所有的表數據都在這一個文件中,如果我們對某張表進行 TRUNCATE 操作,需要將分散在文件中各個地方的數據刪除。首先這樣做性能就不好,其次 TRUNCATE 操作會在該文件中產生很多空閑的碎片空間,並且並不會減少共享表空間文件 ibdata1 的大小。

不能理解的話,可以想像 Java 里的標記-清理垃圾回收算法,該算法會在清理的時候造成大量的內存碎片,不利於提高後期的內存利用率

而對於獨佔表空間來說,從始至終一整張表的數據都只存儲在一個文件,比起共享表空間誰更容易清理並且還能釋放磁盤空間,簡直是一目了然。所以,對於獨佔表空間來說,TRUNCATE 的性能會更好。

除此之外,獨佔表空間能夠提升單張表的最大容量限制,這塊可能不是很好理解,為什麼獨佔表空間還有這個功效?在這裡你只需要記住這個結論就好了,後文講到頁相關的東西時,我們會具體的論證。

了解了表空間的概念之後,我們就可以繼續深入了解數據在表空間內到底是怎麼存儲的了。

深入表空間文件內部

其實在很早之前我講 InnoDB的內存架構 時我就講過,在 InnoDB 中,頁是其數據管理的最小單位。所以講道理我們應該從其最小的部分開始,但是之前已經專門寫過一篇文章來討論頁了,所以在這裡就不贅述了。

表空間由一堆的**頁(Pages)**組成,並且每張頁的大小是相等的,頁大小默認為 16K,當然這個大小可以調整。

頁大小可以通過配置項 innodb_page_size 根據業務的實際情況進行調整,可以選擇的大小分別為 4K、8K、16K、32K和64K

一堆頁組合在一起,就變成了區(Extents)

每個的大小是固定的。當我們設置了不同的 innodb_page_size 時,每個區(Extents)內所包含的頁的數量、和對應的固定區大小都不同,具體的情況如下圖所示。

innodb_page_size 為 4K、8K或者16K時,其對應的區(Extents)大小為1M;當其頁大小為32K時,區大小為2M;當頁大小為64K時,區大小為4M。

MySQL 5.6的時候其實只支持 4K、8K和16K,至於上面說到的32K和64K,是在 MySQL 5.7.6 之後添加的。

隨着頁和區大小的變動,每個區內所能容納的 頁數量 也會隨之改變。舉個例子,當 innodb_page_size 的值為 16K 時,每個區就包含 64 頁;當其為 8K 時,每個區包含 128 頁;當其為 4K 時,每個區就會包含 256 頁。

上面聊過,一頁一頁的數據組成了,而一個一個區則組成了段(Segments)

邏輯上,InnoDB 的表空間就是由一個一個這樣的段(Segment)組成的。隨着數據量的持續增長需要申請新的空間時,InnoDB 會先請求32個頁,之後便會直接分配一整個區(Extents) 。甚至在某個很大的 Segment 內,還會一次性分配 4 個區。

默認情況下,InnoDB 會給每個索引分配兩個段(Segment)。一個用於存儲索引中的非葉子結點,另一個用於存儲葉子結點。

表空間的分類

上面大概介紹了兩種表空間類別,分別是系統表空間、獨佔表空間。接下來就需要詳細的了解一下各個表空間分類的細節了。

系統表空間

當我們開啟了innodb_file_per_table 這個配置項(默認就是開啟的)之後,系統表空間內就只用於存儲 Change Buffer 相關的數據。而當我們將其關閉之後,系統表空間內就會存儲表和索引相關的數據。當然,在 MySQL 8.0之前,獨佔表空間內還包含了 Double Write Buffer(兩次寫緩衝),但在 MySQL 8.0.20 之後被移了出去,存放在了一個單獨的文件中。

默認情況下,系統表空間只會有一個叫 ibdata1 的數據文件,當然,它是允許有多個文件存在的。這所有的屬性包括文件名稱、文件大小都是通過配置項目 innodb_data_file_path 來制定的,舉個例子:

innodb_data_file_path=ibdata1:10M:autoextend

這裡指明了系統表空間的文件名為ibdata1 ,初始化大小為10M 。你可發現了,這個 autoextend 是個什麼鬼?

剛剛說到,初始大小是 10M ,那麼隨着 MySQL 的運行,其數據量會慢慢的增長,數據文件必須要申請更多的空間來存儲數據。而定義了 autoextend InnoDB 就會幫我們自動對數據文件進行擴容,每次擴容申請 8M 的空間。當然,這個 8M 也是可以配置的,我們可以通過配置項 innodb_autoextend_increment 來配置。

獨佔表空間

這塊其實上面在引入的時候已經介紹的差不多了,這裡簡單的總結一下就好。當配置項 innodb_file_per_table 開啟時(現在是默認開啟的),每張表的數據都會存儲自己單獨的數據文件中。

常規表空間

這個暫時不用了解,知道常規表空間跟系統表空間類似,也是一個共享的存儲空間就好。

Undo 表空間

這裡主要存儲 Undo Logs,有了 Undo Logs 我們就可以在事務出錯之後快速的將更改回滾。InnoDB 會默認給 Undo 表空間創建兩個數據文件,如果沒有特別指定,其文件名默認為 undo_001undo_002

至於這兩個數據文件的具體存放路徑,可以通過配置項 innodb_undo_directory 來指定。當然,如果沒有指定,Undo 表空間的數據文件就會放在 InnoDB 的默認數據目錄下,通常來說是 /usr/local/mysql

而這兩個 Undo 表空間數據文件的初始大小,在 MySQL 8.0.23 之前是由 InnoDB 的頁大小來決定的,具體的情況如下圖:

而在 MySQL 8.0.23 之後,Undo 表空間的初始化大小都是 16M 了。至於 Undo 表空間的擴容,不同的版本也有不通的處理方式。

在 MySQL 8.0.23 之前,每次擴容是申請 4 個區(Extends),按照之前的討論,如果頁大小為 16 K,那麼對應到區就是 1M,換句話說,每次擴容申請 4M 的空間,當然這個具體的大小會根據頁大小的變化而變化,這個在上文提到過在此就不再贅述。

而在 MySQL 8.0.23 之後,每次最少都要擴容 16 M的空間。而且,為了防止數據量爆髮式的增長,InnoDB 對擴容的容量會做一個動態的調整。

如果本次擴容和上次擴容時間差小於 0.1 秒,則擴容的空間會加倍,也就是變成 32 M。如果多次擴容的時間差都小於 0.1 秒,這個 加倍 的操作會 累加,直到達到上限 256M

那你可能會說,那如果某段時間剛好請求量比較大,使得擴容的容量達到了最大的 256 M,那後續請求量下去了呢?難道還是申請 256 M嗎?這顯的不太合理。所以 InnoDB 判斷如果兩次擴容間隔大於 0.1 秒,就會將擴容的容量減半,直到減少到最小限制 16 M。

臨時表空間

臨時表空間內的數據,顧名思義都是臨時的。

你在說屁話…

它分為兩個部分,分別是:

  • Session 臨時表空間
  • 全局臨時表空間

對於 Session 臨時表空間,裏面會存儲由用戶或者優化器創建的臨時表。對於每個 Session 來說,InnoDB 最多會分配兩個數據文件(表空間),分別用於存儲用戶創建的臨時表和優化器創建的內部臨時表。當 Session 失效之後,這些已分配的數據文件會被 Truncate 然後放到一個 數據文件池 中。

這個操作其實跟其他的池化技術沒有區別,值得注意的是,這些文件被 Truncate 了之後大小並不會發生變化。而這個數據文件池會在 MySQL 服務器啟動的時候創建,裏面會默認扔進去 10 個文件,每個文件的默認大小為 5 頁。

而對於全局臨時表空間,裏面會存對臨時表做了改動的回滾段(Rollback Segment),其初始化的大小大約是 12 M,同樣會在 MySQL 服務器啟動的時候創建。

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

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