MySQL 8.0 對數據字典進行了重構,用戶表、數據字典表、MySQL 其它系統表的元數據都統一保存到 mysql 庫的數據字典表中了。
mysql 庫中,除了 general_log、slow_log 2 個日志表,其它所有表的存儲引擎都是 InnoDB,伴隨而來的是 ddl 終于能夠支持原子操作了。
以 DROP TABLE t1, t2 為例,不會出現 t1 表刪除成功,t2 表刪除失敗的情況,而是要么都刪除成功,要么都刪除失敗。
本文我們就來聊聊 MySQL 8.0 中的數據字典表。
本文內容基于 MySQL 8.0.29 源碼。
目錄
- 1. 概述
- 2. 數據字典表有哪些?
- 3. 數據字典表元數據在哪里?
- 4. 創建數據字典表
- 5. 打開數據字典表
- 6. 總結
正文
1. 概述
MySQL 8.0 重構數據字典之后,廢除了 MySQL 5.7 中用于保存元數據的磁盤文件:.frm、.par、.TRN、.TRG、.isl、db.opt、ddl_log.log。
如果想要了解上面這些磁盤文件都保存了什么元數據,可以參照 MySQL 官方文檔:https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-file-removal.html
這些文件被廢除之后,原本保存到這些文件中的元數據,都保存到數據字典表中了。
數據字典表本身也大變樣了:
- 數據字典表不再位于 innodb 系統表空間,而是遷移到 mySQL 庫中,mysql 庫位于 mysql 表空間,磁盤文件為 mysql.ibd。
- SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS 這 4 個數據字典表也不再完全依賴硬編碼在源碼中的元數據了,而是和其它表一樣,使用保存在 mysql 庫的數據字典表中的元數據。
上面 4 個數據字典表的名字也發生了變化,后面會介紹。2. 數據字典表有哪些?
按照官方文檔的定義,MySQL 8.0 一共有 31 張數據字典表:
dd_properties innodb_ddl_log catalogs character_sets check_constraints collations column_statistics column_type_elements columns events foreign_key_column_usage foreign_keys index_column_usage index_partitions index_stats indexes parameter_type_elements parameters resource_groups routines schemata st_spatial_reference_systems table_partition_values table_partitions table_stats tables tablespace_files tablespaces triggers view_routine_usage view_table_usage
上面只是簡單列出了數據字典表的表名,如果想了解每個表存放了什么內容,可以參照官方文檔:https://dev.mysql.com/doc/refman/8.0/en/system-schema.html
默認情況下,我們是看不到數據字典表的,需要滿足以下條件才能看到:
- 源碼編譯 Debug 版本 MySQL,以使用 cmake 編譯為例,需要帶上 -DCMAKE_BUILD_TYPE=Debug 編譯選項。
- 連接 MySQL 之后,先執行下面的 SQL 告訴 MySQL 跳過數據字典表的權限檢查:
SET SESSION debug = '+d,skip_dd_table_access_check'
滿足以上 2 個條件之后,執行下面這條 SQL 就可以看到所有數據字典表了:
SELECT a.name AS db_name, b.* FROM mysql.schemata AS a INNER JOIN mysql.tables AS b ON a.id = b.schema_id WHERE b.schema_id = 1 AND b.hidden = 'System' ORDER BY b.id
執行上面的 SQL 列出來的表有 32 個,其中 INNODB_dynamic_metadata 表不屬于數據字典表。
上面列出的數據字典表中,有 4 個需要重點介紹,因為不管是數據字典表本身,還是用戶表,都離不開這 4 個表:
- tables:存儲表的元數據,包括表空間 ID、數據庫 ID、表 ID、表名、表注釋、行格式等信息,對應 MySQL 5.7 中的數據字典表 SYS_TABLES。
- columns:存儲表中字段的元數據,包括表 ID、字段 ID、字段名、字段注釋、字段類型、是否自增等信息,對應 MySQL 5.7 中的數據字典表 SYS_COLUMNS。
- indexes:存儲表的索引元數據,包括表空間 ID、表 ID、索引 ID、索引名、索引注釋、是否是隱藏索引等信息,對應 MySQL 5.7 中的數據字典表 SYS_INDEXES。
- index_column_usage:存儲索引中字段的元數據,包括索引 ID、字段 ID、字段在索引中的編號(從 1 開始)、索引字段長度(如果是前綴索引字段,則是前綴的長度)、索引字段排序、是否隱藏,共 6 個字段,對應 MySQL 5.7 中的數據字典表 SYS_FIELDS。
這個表中沒有包含更詳細的字段信息,如果需要,可以通過字段 ID 到 columns 表獲取。
index_column_usage 和 SYS_FIELDS 表不完全一樣,有 2 點需要說明:
- index_column_usage 包含 6 個字段,比 SYS_FIELDS 多 3 個字段:
order:表示索引字段的排序。
length:hidden = 0 時,表示索引字段長度,或前綴索引字段的前綴長度;hidden = 1 時,字段值為 NULL。
hidden:0 表示索引中該字段由用戶定義;1 表示索引中該字段是 MySQL 給加上的。以下是一個測試表,圖中 name 是從 columns 表中連表查詢得到的,其它都是 index_column_usage 表的字段。
CREATE TABLE `t5` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `str1` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL DEFAULT '', `i1` int NOT NULL DEFAULT '0', `str2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL DEFAULT '', `i2` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `idx_i1` (`i1`) USING BTREE, KEY `idx_str1` (`str1`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
index_id = 310 是主鍵索引,hidden = 0 的記錄是主鍵字段;hidden = 1 的記錄是主鍵索引中的其它字段,也就是表中的字段。
index_id = 312 是二級索引,其中 str1 是前綴索引字段,前綴長度為 255 * 3(utf8 一個字符最多占用的字節數) = 765,hidden = 0 表示 str1 是用戶定義的二級索引字段;hidden = 1 的記錄是 MySQL 自己增加到二級索引中的主鍵字段。
- index_column_usage 表中的 ordinal_position 表示編號,從 1 開始;SYS_FIELDS 中的 POS 表示序號,從 0 開始。
除了在 Debug 版本的 MySQL 中設置跳過數據字典表的權限檢查之外,還可以通過 information_schema 數據庫中的表或視圖查看其對應的數據字典表:
數據字典表information_schema 表或視圖tablesINNODB_TABLEScolumnsINNODB_COLUMNSindexesINNODB_INDEXESindex_column_usage
INNODB_FIELDS…………
3. 數據字典表元數據在哪里?
數據字典表用于存儲用戶表的元數據,這個比較好理解,因為創建用戶表的時候,所有數據字典表都已經存在了,把用戶表的各種元數據插入到相應的數據字典表就可以了。
數據字典表本身的元數據也會保存到數據字典表里,但是某個數據字典表創建的時候,有一些數據字典表還沒有創建,這就有問題了。
我們以 columns、indexes 這 2 個數據字典表為例來說明:columns 表先于 indexes 表創建,columns 表創建成功之后,需要把索引元數據保存到 indexes 表中,而此時 indexes 表還沒有創建,columns 表的索引元數據自然也就沒辦法保存到 indexes 表中了。
MySQL 解決這個問題的方案是引入一個中間層,用于臨時存放所有數據字典表的各種元數據,等到所有數據字典表都創建完成之后,再把臨時存放在中間層的所有數據字典表的元數據保存到相應的數據字典表中。
這里所謂的中間層實際上是一個存儲適配器,源碼中對應的類名為 Storage_adapter,這是一個實現了單例模式的類。
MySQL 在初始化數據目錄的過程中,Storage_adapter 類的實例屬性 m_core_registry 就是所有數據字典表元數據的臨時存放場所。
4. 創建數據字典表
我們安裝 MySQL 完成之后,想讓 MySQL 運行起來,要做的第一件事就是初始化 MySQL,實際上就是初始化 MySQL 數據目錄。
初始化過程會創建 MySQL 運行時需要的各種表空間、數據庫、表,其中就包含數據字典表。
創建數據字典表的過程分為 3 個步驟進行:
第 1 步,把代表每個數據字典表的 Object_table 對象注冊到 System_tables 類的實例屬性 m_registry 中。
除了數據字典表,m_registry 中還包含了 mysql 庫中的其它 MySQL 系統表。
第 2 步,循環 m_registry 中的所有表,通過 Object_table 得到數據字典表的 DDL,然后調用 dd::execute_query() 執行 DDL 語句創建數據字典表。
dd::execute_query() 創建數據字典表的過程中,會把表的元數據臨時存放到 Storage_adapter 類的實例屬性 m_core_registry 中,而不會保存到各種元數據對應的數據字典表中,這么做的原因在上一小節中介紹數據字典表的元數據在哪里時,已經介紹過了,這里不再贅述。
dd::execute_query() 執行完一個數據字典表的 DDL 語句之后,這個數據字典表在表空間中就已經存在了,m_registry 中的所有表都處理完成之后,所有數據字典表就都存在了。
第 3 步,循環 m_registry 中的所有表,把每個表本身的元數據(數據庫 ID、表 ID、表名、注釋、字段數量等)保存到 mysql.tables 數據字典表中,然后把表的字段、索引等元數據保存到對應的數據字典表中。
所有數據字典表的元數據都從 Storage_adapter 類的實例屬性 m_core_registry 中讀取。
經過 3 個步驟的通力協作,所有數據字典表的元數據就都保存到數據字典表中了,這個雞生蛋、蛋生雞的問題,就這樣通過引入外力(m_core_registry)解決了。
5. 打開數據字典表
數據字典表保存著 MySQL 運行過程中需要的一系列關鍵數據,使用頻次很高,MySQL 啟動過程中就會把數據字典表的元數據都加載到內存中,這就是打開表的過程。
也就是說,打開數據字典表是在 MySQL 啟動過程中完成的。
前面我們介紹過,數據字典表的元數據也是保存在數據字典表中的。
MySQL 啟動過程中,要先打開數據字典表才能拿到數據字典表的元數據,而要拿到數據字典表的元數據,又必須先打開數據字典表。
這個過程很繞,不是很好理解,我們來打個比方:數據字典表是一個房間,數據字典表的元數據是打開房間門的鑰匙。
現在問題來了,因為 MySQL 把數據字典表的元數據保存在數據字典表中,這就相當于把打開房間門的鑰匙落在房間里了。
要想打開房間,必須先拿到鑰匙,而要想拿到鑰匙又必須先打開房間,這樣一轉換,問題是不是更好理解點了?
我們先來想想怎么解決房間和鑰匙問題,如果把打開房間的鑰匙落在房間里了,有哪些辦法可以解決?
我能想到的有以下 3 種解決方案:
- 暴力破解,把鎖撬開。
- 找專業的開鎖師傅把鎖打開。
- 用備用鑰匙開門。這個方法最好,但是有個前提條件:已經提前準備好了備用鑰匙。
MySQL 里沒有前 2 種方案,而是留了一把備用鑰匙,也就是第 3 種方案,接下來我們看看 MySQL 打開數據字典表的過程:
第 1 步,和創建數據字典表一樣,把代表每個數據字典表的 Object_table 對象注冊到 System_tables 類的實例屬性 m_registry 中。
每個數據字典表的 Object_table 對象中,都定義了這個表的表名、字段、索引、外鍵等信息。
Object_table 對象中保存的并不是 DDL 語句,卻類似于我們建表時的 DDL 語句。
下面這個例子是源碼中表空間數據字典表 mysql.tablespaces Object_table 對象中定義的該表的信息:
Tablespaces::Tablespaces() { // 表名 m_target_def.set_table_name("tablespaces"); // 字段 m_target_def.add_field(FIELD_ID, "FIELD_ID", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT"); m_target_def.add_field(FIELD_NAME, "FIELD_NAME", "name VARCHAR(268) NOT NULL COLLATE " + String_type(Object_table_definition_impl::name_collation()->m_coll_name)); m_target_def.add_field(FIELD_OPTIONS, "FIELD_OPTIONS", "options MEDIUMTEXT"); m_target_def.add_field(FIELD_SE_private_DATA, "FIELD_SE_PRIVATE_DATA", "se_private_data MEDIUMTEXT"); m_target_def.add_field(FIELD_COMMENT, "FIELD_COMMENT", "comment VARCHAR(2048) NOT NULL"); m_target_def.add_field(FIELD_ENGINE, "FIELD_ENGINE", "engine VARCHAR(64) NOT NULL COLLATE utf8_general_ci"); m_target_def.add_field(FIELD_ENGINE_ATTRIBUTE, "FIELD_ENGINE_ATTRIBUTE", "engine_attribute JSON"); // 索引 m_target_def.add_index(INDEX_PK_ID, "INDEX_PK_ID", "PRIMARY KEY(id)"); m_target_def.add_index(INDEX_UK_NAME, "INDEX_UK_NAME", "UNIQUE KEY(name)"); // 如果有外鍵等其它信息,也會加在這里 }
第 2 步,循環 m_registry 中的所有表,通過 Object_table 得到數據字典表的 DDL,然后調用 dd::execute_query() 執行 DDL 語句創建數據字典表。
和創建數據字典表中的第 2 步不一樣,dd::execute_query() 執行 DDL,并不會真正的創建表,只是為了生成數據字典表元數據,并把元數據保存到 Storage_adapter 類的實例屬性 m_core_registry 中。
保存到 m_core_registry 中的數據字典表元數據,就是我們前面說的備用鑰匙,有了這把備用鑰匙,就能打開數據字典表了。
第 3 步,循環 m_registry 中的所有表,通過第 2 步生成的數據字典表元數據,去 mysql 表空間中(表空間文件:mysql.ibd)讀取各個數據字典表的元數據。
這一步執行完成之后,所有數據字典表的元數據都被加載到內存中了,數據字典表都被打開了。
第 4 步,循環 m_registry 中的所有表,把數據字典表的元數據從 m_core_registry 刪除。
第 5 步,循環 m_registry 中所有的表,把從表空間中讀取出來的數據字典表的元數據存入 m_core_registry 中。
不過,這一步存入 m_core_registry 的并不是所有數據字典表的元數據,而是 22 個核心(CORE)數據字典表的元數據:
catalogs character_sets check_constraints collations column_statistics column_type_elements columns foreign_key_column_usage foreign_keys index_column_usage index_partitions indexes resource_groups schemata table_partition_values table_partitions tables tablespace_files tablespaces triggers view_routine_usage view_table_usage
1 ~ 5 步執行完成之后,m_core_registry 中就只包含上面 22 個核心數據字典表的元數據了,有了這些表的元數據,就可以打開其它所有表了。
第 6 步,調用 dd::execute_query() 執行 FLUSH TABLES 關閉已經打開的所有數據字典表、非數據字典表,后續就可以用從數據字典表中讀取出來的元數據來打開數據字典表和其它所有需要的表了。
到這里,打開數據字典表的大體流程就已經介紹完了,也許大家會有疑問:
第 2 步調用 dd::execute_query() 執行 DDL,已經拿到了數據字典表的元數據。
為了區分,把這里拿到的元數據叫作備用元數據。
第 3 步根據備用元數據打開數據字典表,從表空間中讀取到數據字典表的元數據。
同樣為了區分,把這里拿到的元數據叫作原配元數據。
第 4 步從 m_core_registry 中刪除備用元數據。第 5 步把原配元數據存入 m_core_registry。
數據字典表的備用元數據和原配元數據不是一樣的嗎?為什么還要用原配元數據替換備用元數據,這是不是多此一舉?
我沒有逐個對比備用元數據和原配元數據是否完全一樣,這是個不小的工程。不過,既然源碼中這么實現,那應該是有它的原因,只是我還沒有發現。如果后面發現其中的原因,我會再補充到我的博客中。
6. 總結
要理解 MySQL 8.0 中的數據字典表,核心是理解以下 2 點:
- 初始化數據目錄時,數據字典表的元數據是怎么存放到數據字典表中的?
這主要是借助了 Storage_adapter 類實例的 m_core_registry 屬性。
在創建數據字典表的過程中,先創建每個數據字典表,并把元數據臨時存放到 m_core_registry 中,所有數據字典表都創建成功之后,最后再一次性把所有數據字典表的元數據保存到對應的數據字典表中。
- MySQL 啟動時,怎么用數據字典表的元數據打開數據字典表?
這同時借助了硬編碼在源碼中的數據字典表定義,以及 Storage_adapter 類實例的 m_core_registry 屬性。
MySQL 啟動過程中,先通過 Object_table 得到創建數據字典表的 DDL,調用 dd::execute_query() 執行 DDL,拿到元數據(備用原數據),把備用元數據臨時存放到 m_core_registry 屬性中,再通過備用元數據打開數據字典表。