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.frm
和 student.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_001
和 undo_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詳細的源碼解析,持續更新中。
如果你覺得這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言。