InnoDB 表空間
這應該是 MySQL 原理中最底層的部分了,我們存在 MySQL 中的資料,到底在磁碟上長啥樣。你可能會說,資料不都儲存在聚簇索引中嗎?但很遺憾,你並沒有回答我的問題。我會再問你,那聚簇索引在磁碟上又長啥樣?
就像 Redis 的 RDB 檔案,最終落在磁碟上就是一個真真切切的dump.rdb
檔案,而 MySQL 就顯得有點迷,我們只知道通過 SQL 去拿資料,並不知道資料最終是以什麼方式進行儲存的。當然,瞭解其底層的儲存邏輯,並不僅僅是為了滿足好奇心這麼簡單。
其底層的儲存方式,會影響到聚簇索引中資料的儲存,進而影響到 MySQL 的 DML(DataManipulationLanguage) 效能,所以對底層儲存邏輯有個清晰的認知,就能夠在某些對效能有著極致追求的場景下,幫助我們對 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 中有個概念叫表空間,你大概率是很難理解的。
像上文描述的這種每張表都有自己單獨的資料儲存檔案的,叫獨佔表空間
那資料什麼時候儲存在系統表空間,又什麼時候儲存在獨佔表空間呢?
這個可以通過 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詳細的原始碼解析,持續更新中。
如果你覺得這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言。