MySQL InnoDB 磁盤架構(gòu):如何管理和存儲各數(shù)據(jù)?系統(tǒng)表、獨立表、通用表、撤銷表、臨時表空間是什么?表和索引如何管理?
MySQL innoDB 引擎架構(gòu)可以分為兩大塊,分別是內(nèi)存架構(gòu)(In-Memory Structure)和磁盤架構(gòu)(On-Disk Structure)。
圖 1
書接上回《MySQL InnoDB 架構(gòu) Buffer Pool、Change Buffer、自適應(yīng)哈希索引、Log Buffer》,我們掌握了 InnoDB 引擎的內(nèi)存架構(gòu)。
數(shù)據(jù)最終要持久化到磁盤,其磁盤架構(gòu)設(shè)計融合了復(fù)雜的存儲結(jié)構(gòu)和精巧的機制,本文將深入剖析其核心模塊的設(shè)計原理,并通過圖片輔助理解。
MySQL 到底是怎么管理和存儲各種各樣的數(shù)據(jù)呢?比如創(chuàng)建一張表、索引、表中的每一行數(shù)據(jù)、查詢過程中臨時存儲的數(shù)據(jù)都存在哪里,又如何管理?
這一切都歸功于 MySQL 的 Tablespaces (表空間)的設(shè)計,內(nèi)容較多,本篇就關(guān)于以下類型 Tablespaces (表空間)作用和實現(xiàn)原理展開:
- 系統(tǒng)表空間(System Tablespace)
- 獨立表空間(File-Per-Table Tablespaces)
- 通用表空間(General Tablespaces)
- 撤銷表空間(Undo Tablespaces)
- 臨時表空間(Temporary Tablespaces)
- Tables(表)
- Indexes(索引)
Tablespaces (表空間)
表空間可以看做是 InnoDB 存儲引擎邏輯結(jié)構(gòu)的最高層,所有的數(shù)據(jù)都存放在表空間中,稱之為表空間(tablespace)。
從物理文件的分類來看:
- 日志文件(Undo Log、Redo Log)。
- 系統(tǒng)表空間(System Tablespace)文件 ibdata1。
- Undo tablespace 。
- 獨立表空間(File-Per-Table Tablespaces)
- 通用表空間(General Tablespaces)
- 臨時表空間文件(Temporary Tablespaces)
所以表空間根據(jù)不同的場景也分了多種類型,我分別介紹下……
系統(tǒng)表空間(System Tablespace)
默認配置下會有一個初始大小為 10MB,名為 ibdata1 的文件。該文件就是默認的表空間文件(tablespace file)。
系統(tǒng)表空間是 Change Buffer 的存儲區(qū)域。
如果表是在系統(tǒng)表空間而非獨立表空間或通用表空間中創(chuàng)建的,它也可能包含表和索引數(shù)據(jù)。
增加系統(tǒng)表空間大小的最簡單方法是將其配置為自動擴展。
為此,在 innodb_data_file_path
設(shè)置中為最后一個數(shù)據(jù)文件指定 autoextend
屬性,并重啟服務(wù)器。
innodb_data_file_path=ibdata1:10M:autoextend
為避免系統(tǒng)表空間過大,可考慮使用獨立表空間或通用表空間存儲數(shù)據(jù)。
獨立表空間是默認的表空間類型,在創(chuàng)建 InnoDB
表時會隱式使用。
獨立表空間(File-Per-Table Tablespaces)
獨立表空間,顧名思義,就是用戶創(chuàng)建的表空間,如果開啟獨立表空間參數(shù),那么一個表空間會對應(yīng)磁盤上的一個物理文件,每張表對應(yīng)一個文件,支持事務(wù)獨立管理。
其實表空間文件內(nèi)部還是組織為更復(fù)雜的邏輯結(jié)構(gòu),自頂向下可分為 segment(段)、extent(區(qū))和 page(頁)。
page 則是表空間數(shù)據(jù)存儲的基本單位,innodb 將表文件(xxx.ibd)按 page 切分,依類型不同,page 內(nèi)容也有所區(qū)別,最為常見的是存儲數(shù)據(jù)庫表的行記錄。
表空間下一級稱為 segment。segment 與數(shù)據(jù)庫中的索引相映射。
Innodb 引擎內(nèi),每個索引對應(yīng)兩個 segment:管理葉子節(jié)點的 segment 和管理非葉子節(jié)點 segment。
創(chuàng)建索引中很關(guān)鍵的步驟便是分配 segment,Innodb 內(nèi)部使用 INODE 來描述 segment。
segment 的下一級是 extent,extent 代表一組連續(xù)的 page,默認為 64 個 page,大小 1MB。
InnoDB 存儲引擎的邏輯存儲結(jié)構(gòu)大致如圖 2 所示。
圖 2
默認情況下 InnoDB 存儲引擎有一個共享表空間 ibdata1,即所有數(shù)據(jù)都存放在這個表空間內(nèi)。
如果用戶啟用了參數(shù)innodb_file_per_table
,則每張表內(nèi)的數(shù)據(jù)可以單獨放到一個表空間內(nèi)。
如果啟用了innodb_file_per_table
的參數(shù),需要注意的是每張表的表空間內(nèi)存放的只是數(shù)據(jù)、索引和插入緩沖 Bitmap 頁.
其他類的數(shù)據(jù),如回滾(undo)信息,插入緩沖索引頁、系統(tǒng)事務(wù)信息,二次寫緩沖(Double write buffer)等還是存放在原來的系統(tǒng)表空間內(nèi)。
通用表空間(General Tablespaces)
通用表空間是一種共享的 InnoDB
表空間,通過 CREATE TABLESPACE
語法創(chuàng)建。
通用表空間提供以下功能:
- 類似于系統(tǒng)表空間,通用表空間是一種共享表空間,能夠存儲多張表的數(shù)據(jù)。
- 通用表空間在內(nèi)存占用上可能優(yōu)于獨立表空間。服務(wù)器會在表空間生命周期內(nèi)將表空間元數(shù)據(jù)保留在內(nèi)存中。相較于相同數(shù)量的表分散在多個獨立表空間中,更少的通用表空間內(nèi)存儲多張表能減少表空間元數(shù)據(jù)的內(nèi)存消耗。
通用表空間通過 CREATE TABLESPACE
語法創(chuàng)建。
CREATE TABLESPACE tablespace_name
[ADD DATAFILE 'file_name']
[FILE_BLOCK_SIZE = value]
[ENGINE [=] engine_name]
通用表空間有什么不足?
通用表空間限制有以下限制:
- 現(xiàn)有的表空間無法更改為通用表空間。
- 不支持創(chuàng)建臨時通用表空間。
- 通用表空間不支持臨時表。
- 不支持將表分區(qū)放置在通用表空間中。
- 在復(fù)制環(huán)境中,如果源和副本位于同一主機上,則不支持使用
ADD DATAFILE
子句,因為這會導(dǎo)致源和副本在同一位置創(chuàng)建同名的表空間,而這是不被支持的。
撤銷表空間(Undo Tablespaces)
MySQL InnoDB 引擎的 Undo Tablespaces(撤銷表空間)是磁盤架構(gòu)設(shè)計中用于管理事務(wù)回滾日志(Undo Log)的核心組件。
唐二婷:InnoDB 引擎的 Undo Tablespaces(撤銷表空間)有啥用?
Undo 日志(Undo Log)主要用于事務(wù)異常時的數(shù)據(jù)回滾,在磁盤上 undo 日志保存在 Undo Tablespaces 中。
- 事務(wù)回滾與 MVCC 支持Undo 表空間存儲的 Undo Log 記錄了事務(wù)對數(shù)據(jù)的修改前鏡像,用于:
事務(wù)回滾時恢復(fù)數(shù)據(jù)原狀;
實現(xiàn)多版本并發(fā)控制(MVCC),支持非鎖定一致性讀。
- 分離系統(tǒng)表空間負載在 MySQL 5.7 之前,Undo Log 默認存儲在系統(tǒng)表空間(
ibdata1
)中。
隨著事務(wù)頻繁操作,ibdata1
文件會無限增長且無法自動回收空間。
5.7 及更高版本引入獨立 Undo 表空間,通過物理隔離減輕系統(tǒng)表空間壓力,提升性能。
MySQL 8.0 默認創(chuàng)建 2 個 Undo 表空間文件(undo_001
和 undo_002
),每個初始大小為 16MB,通過參數(shù) innodb_undo_tablespaces
可調(diào)整數(shù)量(范圍 2-127),每個文件初始 16MB,支持自動擴展和截斷回收。
圖片
唐二婷:“Undo 表空間的邏輯層級管理是咋樣的?”
回滾段(Rollback Segments):每個 Undo 表空間包含 128 個回滾段(由 innodb_rollback_segments
控制),每個回滾段管理 1024 個 Undo 段(Undo Segments)。
Undo 頁與日志記錄:Undo 段由多個 16KB 的頁組成,按事務(wù)類型分為 Insert Undo 段(僅用于回滾)和 Update Undo 段(用于 MVCC),前者事務(wù)提交后立即釋放,后者需等待無活躍讀視圖時清除。
通過多 Undo 表空間與回滾段的分區(qū)設(shè)計,理論上支持高達數(shù)萬級并發(fā)事務(wù)(例如:128 表空間 × 128 回滾段 × 1024 Undo 段)。
如下圖所示。
圖片
關(guān)鍵說明:
- 每個 Undo 表空間包含 128 個回滾段
- 每個回滾段管理 1024 個 Undo 段(按事務(wù)類型分類)
- Undo 段由 16KB 頁 組成,存儲具體日志記錄
唐二婷:說說 Undo Log 與 MVCC 的協(xié)作機制
Undo Log 與 MVCC 的協(xié)作機制如下圖所示:
圖片
運作原理:
- 事務(wù)修改前將舊數(shù)據(jù)寫入 Undo Log
- 讀事務(wù)通過 Read View 判斷可見性
- 多版本數(shù)據(jù)通過 Undo Log 鏈回溯訪問
唐二婷:“系統(tǒng)表空間與 Undo 表空間存儲有啥區(qū)別?”
圖片
特性 | Undo 表空間 | 系統(tǒng)表空間(歷史方案) |
存儲內(nèi)容 | 僅 Undo Log | 數(shù)據(jù)字典、雙寫緩沖、Undo Log 等混合內(nèi)容 |
空間管理 | 支持自動截斷,避免文件膨脹 | 無法自動回收,需手動調(diào)整或重建 |
性能影響 | 減少 I/O 競爭,提升并發(fā)處理能力 | 高頻事務(wù)易導(dǎo)致文件過大,性能下降 |
版本支持 | MySQL 5.7+ 默認方案 | MySQL 5.6 及更早版本 |
臨時表空間(Temporary Tablespaces)
InnoDB 臨時表空間分為 會話臨時表空間 和 全局臨時表空間,分別承擔(dān)不同角色:
- 會話臨時表空間(Session Temporary Tablespaces)
- 用途:存儲用戶顯式創(chuàng)建的臨時表(CREATE TEMPORARY TABLE)以及優(yōu)化器生成的內(nèi)部臨時表(如排序、分組操作)。
- 生命周期:會話斷開時自動截斷并釋放回池,文件擴展名為
.ibt
,默認位于#innodb_temp
目錄。 - 分配機制:首次需要創(chuàng)建磁盤臨時表時,從預(yù)分配的池中分配(默認池包含 10 個表空間文件),每個會話最多分配 2 個表空間(用戶臨時表與優(yōu)化器內(nèi)部臨時表各一)。
- 全局臨時表空間(Global Temporary Tablespace):
- 用途:存儲用戶臨時表的回滾段(Rollback Segments),支持事務(wù)回滾操作。
- 文件配置:默認文件名為
ibtmp1
,初始大小 12MB,支持自動擴展,由參數(shù)innodb_temp_data_file_path
控制路徑與屬性。 - 回收機制:服務(wù)器重啟時自動刪除并重建,意外崩潰時需手動清理。
Temporary Tablespaces 物理結(jié)構(gòu)
圖片
圖示說明:
- 全局臨時表空間:
ibtmp1
存儲用戶臨時表的回滾段 - 會話臨時表空間:
#innodb_temp
目錄下預(yù)分配 10 個.ibt
文件池(默認配置) - 每個會話最多激活 2 個臨時表空間(用戶臨時表 + 優(yōu)化器內(nèi)部臨時表)。
會話級臨時表空間生命周期
圖片
關(guān)鍵點:
- 首次需要磁盤臨時表時從池中分配
- 會話斷開連接后立即歸還空間
- 文件物理保留但內(nèi)容截斷(類似內(nèi)存池機制)
臨時表空間使用查詢流程
前面說過臨時表空間可存儲用戶顯式創(chuàng)建的臨時表(CREATE TEMPORARY TABLE)以及優(yōu)化器生成的內(nèi)部臨時表(如排序、分組操作)。
那它的查詢過程是怎樣的呢?
圖片
Tables(表)
唐二婷:“在 MySQL 如何創(chuàng)建一張表?”
InnoDB
表通過 CREATE TABLE
語句創(chuàng)建;例如:
CREATE TABLE t1 (a INT, b CHAR (20), PRIMARY KEY (a)) ENGINE=InnoDB;
默認情況下, InnoDB
表創(chuàng)建于每表獨立的表空間中。若要在 InnoDB
系統(tǒng)表空間中創(chuàng)建 InnoDB
表,需在創(chuàng)建表前禁用 innodb_file_per_table
變量。
比如,在數(shù)據(jù)庫 test
中創(chuàng)建一個表 show_index
,在 mysql 的 dataDirectory 目錄下就回出現(xiàn)一個名為 show_index.ibd
的數(shù)據(jù)文件。
在單個表的數(shù)據(jù)文件中,數(shù)據(jù)就是以多個頁的形式進行排列。MySQL 默認配置下,每 16K,即為一個頁。
InnoDB 表以 B+樹 組織數(shù)據(jù),每個表對應(yīng)一個 聚簇索引(Clustered Index),數(shù)據(jù)行的物理存儲順序與主鍵順序一致。
若未顯式定義主鍵,InnoDB 會隱式生成一個 6 字節(jié)的 Row ID 作為主鍵。
Row Formats 行格式
唐二婷:表中的每一行數(shù)據(jù)是怎么存儲的?
表的 InnoDB
行格式?jīng)Q定了其行在磁盤上的物理存儲方式。
InnoDB
支持四種行格式,每種格式具有不同的存儲特性。
支持的行格式包括 REDUNDANT
、 COMPACT
、 DYNAMIC
和 COMPRESSED
。其中, DYNAMIC
行格式為默認格式。
唐二婷:它們有啥區(qū)別?
REDUNDANT
和 COMPACT
行格式支持的最大索引鍵前綴長度為 767 字節(jié),而 DYNAMIC
和 COMPRESSED
行格式則支持 3072 字節(jié)的索引鍵前綴長度。
在復(fù)制環(huán)境中,若源服務(wù)器上的 innodb_default_row_format
變量設(shè)置為 DYNAMIC
,而副本上設(shè)置為 COMPACT
,則以下未明確指定行格式的 DDL 語句在源服務(wù)器上執(zhí)行成功,但在副本上會失敗。
Primary Keys 主鍵
建議為創(chuàng)建的每個表定義一個主鍵。在選擇主鍵列時,應(yīng)選擇具有以下特征的列:
- 重要的查詢語句使用的列。
- 列不能為空。
- 從不包含重復(fù)值的列。
- 一旦插入后極少甚至從不更改值的列。
例如,在包含人員信息的表中,你不會將主鍵設(shè)在 (firstname, lastname)
上,因為可能有多個人員擁有相同的姓名,姓名列可能留空,且有時人們會更改姓名。
面對如此多的限制條件,通常沒有明顯的一組列適合作為主鍵,因此你會創(chuàng)建一個帶有數(shù)字 ID 的新列,作為主鍵。
最好的方式就是使用趨勢遞增的數(shù)字作為主鍵。
你也可以 在 InnoDB
表中使用 AUTO_INCREMENT
的列來定義主鍵自動生成。
AUTO_INCREMENT 實現(xiàn)原理是什么?會鎖全表碼?
自增鎖模式通過 innodb_autoinc_lock_mode
變量在啟動時配置。
自增主鍵鎖
“傳統(tǒng)”鎖模式
innodb_autoinc_lock_mode = 0
(“傳統(tǒng)”鎖模式),所有“INSERT 類”語句在向具有 AUTO_INCREMENT
列的表中插入時都會獲得一個特殊的表級 AUTO-INC
鎖。
此鎖通常保持到語句的末尾(而不是事務(wù)的末尾),以確保在給定的 INSERT
語句序列中自動增量值按可預(yù)測和可重復(fù)的順序分配,并確保任何給定語句分配的自動增量值是連續(xù)的。
“連續(xù)”鎖模式
innodb_autoinc_lock_mode = 1
(“連續(xù)”鎖模式),“批量插入”使用特殊的 AUTO-INC
表級鎖,并保持到語句結(jié)束。這適用于所有 INSERT ... SELECT
、 REPLACE ... SELECT
和 LOAD DATA
語句。
這種鎖模式確保,在存在 INSERT
語句且行數(shù)未知(并且自增值在語句執(zhí)行過程中分配)的情況下,任何“ INSERT
-類似”語句分配的所有自增值都是連續(xù)的,并且操作對基于語句的復(fù)制是安全的。
innodb_autoinc_lock_mode = 2
(“交錯”鎖模式)
在這種鎖模式中,沒有“ INSERT
-like”語句使用表級 AUTO-INC
鎖,并且多個語句可以同時執(zhí)行。
這是最快且最可擴展的鎖模式,但在使用基于語句的復(fù)制或從二進制日志重放 SQL 語句的恢復(fù)場景時是不安全的。
在此鎖定模式下,自動增量值在整個并發(fā)執(zhí)行的“ INSERT
-like”語句中保證是唯一的且單調(diào)遞增。
然而,由于多個語句可以同時生成數(shù)字(即,數(shù)字的分配在語句之間交錯進行),任何給定語句插入的行生成的值可能不是連續(xù)的。
Indexes(索引)
InnoDB 的索引分為 聚簇索引 和 二級索引(Secondary Index),均采用 B+樹結(jié)構(gòu):
- 聚簇索引:也稱 Clustered Index。是指關(guān)系表記錄的物理順序與索引的邏輯順序相同。由于一張表只能按照一種物理順序存放,一張表最多也只能存在一個聚集索引。葉子節(jié)點直接存儲行數(shù)據(jù)。
- 二級索引:也叫 Secondary Index。指的是非葉子節(jié)點按照索引的鍵值順序存放,葉子節(jié)點存放索引鍵值以及對應(yīng)的主鍵鍵值。MySQL 里除了 INNODB 表主鍵外,其他的都是二級索引。葉子節(jié)點存儲主鍵值,需通過主鍵回表查詢數(shù)據(jù)。
下圖是一個聚集索引的 B+ Tree 圖。
圖片
1 個 B+ Tree Node,占據(jù)一個頁。
- 在索引頁,頁的主要記錄部分(
User Records
)存放的Record
=record header
+index key
+page pointer
。 - 在數(shù)據(jù)頁,則是按表創(chuàng)建時的
row_format
類型存放完整數(shù)據(jù)行記錄。 row_format 類型分別有:Compact
、Redundant
、Compressed
和Dynamic
。
因此,在聚集索引中,非葉子節(jié)點都為索引頁,葉子節(jié)點為數(shù)據(jù)頁;
在輔助索引中,非葉子節(jié)點和葉子節(jié)點都為索引頁。不同的是,葉子節(jié)點里記錄的是聚集索引中的主鍵 ID 值。
INNODB 表的二級索引,如下圖所示,圖片來自「一樹一溪」:
圖片
注意,在索引頁的 Record 中的page pointer
,指向的是頁,而非具體的記錄行。
并且 Record 的index key
,為指向的 page records 的起始鍵值。
如果主鍵較長,二級索引會占用更多空間,因此擁有較短的主鍵是有利的。
在表空間文件的一個頁的結(jié)構(gòu)上,內(nèi)容布局為:
圖片
在聚集索引中,數(shù)據(jù)頁內(nèi)除了按照主鍵大小進行記錄存放以外,在File header
中,有兩個字段:fil_page_prev
和fil_page_next
, 分別記錄了上一頁/下一頁的偏移量(offset),用以實現(xiàn)數(shù)據(jù)頁在 B+ Tree 葉子位置的雙向鏈表結(jié)構(gòu)。
數(shù)據(jù)如何被查找檢索呢?
通過 B+ Tree 結(jié)構(gòu),可以明顯看到,通過 B+ Tree 查找,可以定位到索引最后指向的數(shù)據(jù)頁,并不能找到具體的記錄本身。
這時,數(shù)據(jù)庫會將該頁加載到內(nèi)存中,然后通過Page Directory
進行二分查找。