本文分享自華為雲社區《MySQL全文索引源碼剖析之Insert語句執行過程》,作者:GaussDB 資料庫。 本文主要介紹MySQL 8.0數據字典的基本概念和數據字典的初始化與啟動載入的主要流程。 MySQL 8.0數據字典簡介 數據字典(Data Dictionary, DD)用來存儲資料庫內部 ...
本文分享自華為雲社區《MySQL全文索引源碼剖析之Insert語句執行過程》,作者:GaussDB 資料庫。
本文主要介紹MySQL 8.0數據字典的基本概念和數據字典的初始化與啟動載入的主要流程。
MySQL 8.0數據字典簡介
數據字典(Data Dictionary, DD)用來存儲資料庫內部對象的信息,這些信息也被稱為元數據(Metadata),包括schema名稱、表結構、存儲過程的定義等。
圖1 MySQL 8.0之前的數據字典
圖片來源:MySQL 8.0 Data Dictionary: Background and Motivation
如圖1所示,MySQL 8.0之前的元數據,分散存儲在許多不同的位置,包括各種元數據文件,不支持事務的表和存儲引擎特有的數據字典等;Server層和存儲引擎層有各自的數據字典,其中一部分是重覆的。
以上的設計導致支持原子的DDL變得很困難,因此MySQL 8.0之前,如果DDL過程中發生crash,後期的恢復很容易出現各種問題,導致表無法訪問、複製異常等。
如圖2所示,MySQL 8.0使用支持事務的InnoDB存儲引擎作來存儲元數據,實現數據字典的統一管理。這個改進消除了元數據存儲的冗餘,通過支持原子DDL,實現了DDL的crash safe。
圖2 MySQL 8.0數據字典
圖片來源:MySQL 8.0: Data Dictionary Architecture and Design
數據字典表都是隱藏的,只有在debug編譯模式下,可以通過設置開關set debug='+d,skip_dd_table_access_check'來直接查看數據字典表。
mysql> set debug='+d,skip_dd_table_access_check'; Query OK, 0 rows affected (0.01 sec) mysql> SELECT name, schema_id, hidden, type FROM mysql.tables where schema_id=1 AND hidden='System'; +------------------------------+-----------+--------+------------+ | name | schema_id | hidden | type | +------------------------------+-----------+--------+------------+ | catalogs | 1 | System | BASE TABLE | | character_sets | 1 | System | BASE TABLE | | check_constraints | 1 | System | BASE TABLE | | collations | 1 | System | BASE TABLE | | column_statistics | 1 | System | BASE TABLE | | column_type_elements | 1 | System | BASE TABLE | | columns | 1 | System | BASE TABLE | | dd_properties | 1 | System | BASE TABLE | | events | 1 | System | BASE TABLE | | foreign_key_column_usage | 1 | System | BASE TABLE | | foreign_keys | 1 | System | BASE TABLE | | index_column_usage | 1 | System | BASE TABLE | | index_partitions | 1 | System | BASE TABLE | | index_stats | 1 | System | BASE TABLE | | indexes | 1 | System | BASE TABLE | | innodb_ddl_log | 1 | System | BASE TABLE | | innodb_dynamic_metadata | 1 | System | BASE TABLE | | parameter_type_elements | 1 | System | BASE TABLE | | parameters | 1 | System | BASE TABLE | | resource_groups | 1 | System | BASE TABLE | | routines | 1 | System | BASE TABLE | | schemata | 1 | System | BASE TABLE | | st_spatial_reference_systems | 1 | System | BASE TABLE | | table_partition_values | 1 | System | BASE TABLE | | table_partitions | 1 | System | BASE TABLE | | table_stats | 1 | System | BASE TABLE | | tables | 1 | System | BASE TABLE | | tablespace_files | 1 | System | BASE TABLE | | tablespaces | 1 | System | BASE TABLE | | triggers | 1 | System | BASE TABLE | | view_routine_usage | 1 | System | BASE TABLE | | view_table_usage | 1 | System | BASE TABLE | +------------------------------+-----------+--------+------------+ 32 rows in set (0.01 sec)
上面查詢得到的表就是隱藏的數據字典表,MySQL的元數據存儲在這些表中。
在release編譯模式下,如果要查看數據字典信息,只能通過INFORMATION_SCHEMA中的視圖來查詢。例如,可以通過視圖information_schema.tables查詢數據字典表mysql.tables。
mysql> select TABLE_SCHEMA,TABLE_NAME,TABLE_TYPE,ENGINE -> from information_schema.tables -> where TABLE_SCHEMA = 'sbtest' limit 1; +--------------+------------+------------+--------+ | TABLE_SCHEMA | TABLE_NAME | TABLE_TYPE | ENGINE | +--------------+------------+------------+--------+ | sbtest | sbtest1 | BASE TABLE | InnoDB | +--------------+------------+------------+--------+ 1 row in set (0.00 sec)
數據字典表的相關代碼
數據字典的代碼位於sql/dd目錄,所有數據字典相關的信息都在dd這個命名空間中,各數據字典表本身的定義位於sql/dd/impl/tables目錄的代碼中,可以理解為數據字典表的元數據在代碼中已經定義好了。
以存儲schema信息的schemata表為例,其類的聲明如下:
class Schemata : public Entity_object_table_impl { public: // ... // 所包含的欄位 enum enum_fields { FIELD_ID, FIELD_CATALOG_ID, FIELD_NAME, FIELD_DEFAULT_COLLATION_ID, FIELD_CREATED, FIELD_LAST_ALTERED, FIELD_OPTIONS, FIELD_DEFAULT_ENCRYPTION, FIELD_SE_PRIVATE_DATA, NUMBER_OF_FIELDS // Always keep this entry at the end of the enum }; // 所包含的索引 enum enum_indexes { INDEX_PK_ID = static_cast<uint>(Common_index::PK_ID), INDEX_UK_CATALOG_ID_NAME = static_cast<uint>(Common_index::UK_NAME), INDEX_K_DEFAULT_COLLATION_ID }; // 所包含的外鍵 enum enum_foreign_keys { FK_CATALOG_ID, FK_DEFAULT_COLLATION_ID }; // ... };
其構造函數定義了該表的名稱、各欄位、索引和外鍵等信息,以及該表預設存儲的數據信息,如下所示:
Schemata::Schemata() { // 表名 m_target_def.set_table_name("schemata"); // 欄位定義 m_target_def.add_field(FIELD_ID, "FIELD_ID", "id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT"); // ... // 索引定義 m_target_def.add_index(INDEX_PK_ID, "INDEX_PK_ID", "PRIMARY KEY (id)"); // ... // 外鍵定義 m_target_def.add_foreign_key(FK_CATALOG_ID, "FK_CATALOG_ID", "FOREIGN KEY (catalog_id) REFERENCES \ catalogs(id)"); // ... // 初始化時額外需要執行的DML語句 m_target_def.add_populate_statement( "INSERT INTO schemata (catalog_id, name, default_collation_id, created, " "last_altered, options, default_encryption, se_private_data) VALUES " "(1,'information_schema',33, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, " "NULL, 'NO', NULL)"); }
在初始化和啟動時,會使用Object_table_definition_impl::get_ddl()函數來獲取m_target_def中信息所生成的DDL語句,創建出schemata表;使用Object_table_definition_impl::get_dml()獲取DML語句,用於初始化表中的數據。
dd::tables::Schemata類的繼承關係,如圖3。所有的數據字典表對應的類,最終都是派生自dd::Object_table,便於統一處理。
圖3 dd::tables::Schemata類
對於這些表中存儲的元數據所對應的對象,或者說這些表中的每一行數據所對應的一個對象,比如一個schema、table、column等,代碼中也有對應的類。
還是以schema為例,它對應的類是dd::Schema,實現類是dd::Schema_impl,代表的是schema這種資料庫內部對象,也是mysql.schemata表中的一行。
所有數據字典中所存儲的對象在代碼中的基類都是dd::Weak_object,如圖4:
圖4 dd::Schema_impl類
schema的id和name在dd::Entity_object_impl中,其他欄位在實現類dd::Schema_impl中。
實現類dd::Schema_impl主要實現了對於元數據對象的各屬性的讀寫訪問,與從數據字典中的元數據表schemata的行記錄中,存取元數據的介面。
主要相關介面如下:
class Weak_object_impl_ : virtual public Weak_object { // ... public: // 存儲記錄到元數據表 virtual bool store(Open_dictionary_tables_ctx *otx); // 刪除元數據表中的記錄 bool drop(Open_dictionary_tables_ctx *otx) const; public: // 從元數據表的記錄中提取各屬性欄位 virtual bool restore_attributes(const Raw_record &r) = 0; // 保存各屬性到元數據表的記錄 virtual bool store_attributes(Raw_record *r) = 0; // 讀取相關對象的信息,如表上的索引等 virtual bool restore_children(Open_dictionary_tables_ctx *) { return false; } // 存儲相關對象的信息 virtual bool store_children(Open_dictionary_tables_ctx *) { return false; } // 刪除相關對象的信息 virtual bool drop_children(Open_dictionary_tables_ctx *) const { return false; } };
dd::Schema_impl主要實現了store_attributes和restore_attributes介面,依據dd::tables::Schemata中的表定義信息,讀取或存儲schema的各個屬性信息。
依據以上介紹的,數據字典表的類與資料庫內部對象的類,結合InnoDB存儲引擎的介面,實現了對於存儲於數據字典各個表中的元數據的讀寫訪問。
例如,存儲新建的database的元數據到schema記憶體對象中:
#0 dd::Schema_impl::store_attributes #1 in dd::Weak_object_impl::store #2 in dd::cache::Storage_adapter::store<dd::Schema> #3 in dd::cache::Dictionary_client::store<dd::Schema> #4 in dd::create_schema #5 in mysql_create_db #6 in mysql_execute_command...
持久化到對應的InnoDB表mysql.schemata中:
#0 ha_innobase::write_row #1 in handler::ha_write_row #2 in dd::Raw_new_record::insert #3 in dd::Weak_object_impl::store #4 in dd::cache::Storage_adapter::store<dd::Schema> #5 in dd::cache::Dictionary_client::store<dd::Schema> #6 in dd::create_schema #7 in mysql_create_db #8 in mysql_execute_command ...
數據字典的初始化
初始化MySQL資料庫實例時,即執行mysqld -initialize時,main函數會啟動一個bootstrap線程來進行數據字典的初始化,並等待其完成。
數據字典的初始化函數入口是dd::bootstrap::initialize,主要流程如下:
圖5 數據字典初始化流程
其中,DDSE指的是Data Dictionary Storage Engine,數據字典的存儲引擎,即InnoDB。DDSE初始化過程主要是對InnoDB進行必要的初始化,並獲取DDSE代碼中預先定義好的表的定義與表空間的定義。
InnoDB預定義的數據字典表:
- innodb_dynamic_metadata
- innodb_table_stats
- innodb_index_stats
- innodb_ddl_log
InnoDB預定義的系統表空間:
- mysql
- innodb_system
InnoDB的ddse_dict_init介面的實現為innobase_ddse_dict_init,會先調用innobase_init_files初始化所需文件並啟動InnoDB。
主要代碼流程如下:
static bool innobase_ddse_dict_init( dict_init_mode_t dict_init_mode, uint, List<const dd::Object_table> *tables,List<const Plugin_tablespace> *tablespaces) { // ... // 初始化文件並啟動InnoDB if (innobase_init_files(dict_init_mode, tablespaces)) { return true; } // innodb_dynamic_metadata表的定義 dd::Object_table *innodb_dynamic_metadata = dd::Object_table::create_object_table(); innodb_dynamic_metadata->set_hidden(true); dd::Object_table_definition *def = innodb_dynamic_metadata->target_table_definition(); def->set_table_name("innodb_dynamic_metadata"); def->add_field(0, "table_id", "table_id BIGINT UNSIGNED NOT NULL"); def->add_field(1, "version", "version BIGINT UNSIGNED NOT NULL"); def->add_field(2, "metadata", "metadata BLOB NOT NULL"); def->add_index(0, "index_pk", "PRIMARY KEY (table_id)"); // ... /* innodb_table_stats、innodb_index_stats、innodb_ddl_log表的定義 */ // ... }
在DDSE初始化並啟動的基礎上,就可以進行剩下的數據字典初始化過程,主要就是創建數據字典的schema和表。這些表的元數據在執行flush_meta_data時進行持久化。
值得註意的是表mysql.dd_properties,它會存儲版本信息等數據字典的屬性,還會存儲其他數據字典表的定義、id、se_private_data等信息,在資料庫啟動時使用。
數據字典初始化整體執行的函數調用總結,如圖6:
圖6 數據字典初始化的函數調用
數據字典的啟動
數據字典的啟動過程所執行的函數與初始化時十分相似,大部分在函數內部通過opt_initialize全局變數來區分初始化和啟動,執行不同的代碼邏輯。
與初始化的主要區別是元數據不再需要生成並持久化到存儲,而是從存儲讀取已有的元數據。InnoDB文件是打開已有的,而不是新建。
數據字典啟動的入口是dd::upgrade_57::do_pre_checks_and_initialize_dd。這裡雖然有'upgrade_57'這種名稱的namespace,但是正常的啟動也是從這裡開始。
與初始化相同,數據字典的啟動也是先準備好DDSE,即啟動InnoDB,然後再進行後面啟動數據字典的步驟。打開數據字典之前,InnoDB會進行數據字典的恢復,確保重啟前的DDL都正常的提交或回滾,數據字典元數據和數據是處於一致的狀態。
dd::upgrade_57::restart_dictionary調用dd::bootstrap::restart,後面的啟動步驟由它來實現,主要過程如下。
註意這裡的創建表,是創建記憶體中的對象,不是物理上新創建一個表。這些表的元數據都已經在初始化時持久化了。
bool restart(THD *thd) { bootstrap::DD_bootstrap_ctx::instance().set_stage(bootstrap::Stage::STARTED); // 獲取預定義的系統tablespace的元數據(mysql和innodb_system) store_predefined_tablespace_metadata(thd); if (create_dd_schema(thd) || // 創建schema:'mysql' initialize_dd_properties(thd) || // 創建mysql.dd_properties表並從中獲取版本號等信息 create_tables(thd, nullptr) || // 創建數據字典中其他的表 sync_meta_data(thd) || // 從存儲讀取數據字典相關的schema、tablespace和表的元數據,進行同步 /* 打開InnoDB的數據字典表(innodb_dynamic_metadata, innodb_table_stats, innodb_index_stats, innodb_ddl_log),載入所有InnoDB的表空間 */ DDSE_dict_recover(thd, DICT_RECOVERY_RESTART_SERVER, d->get_actual_dd_version(thd)) || upgrade::do_server_upgrade_checks(thd) || // 檢查是否能夠升級(如果需要的話,正常啟動不涉及) upgrade::upgrade_tables(thd) || // 升級數據字典表的定義及其中的元數據(如果需要的話,正常啟動不涉及) repopulate_charsets_and_collations(thd) || // 更新charset和collation信息 verify_contents(thd) || // 驗證數據字典內容 update_versions(thd, false)) { // 更新版本信息到dd_properties表 return true; } // ... bootstrap::DD_bootstrap_ctx::instance().set_stage(bootstrap::Stage::FINISHED); LogErr(INFORMATION_LEVEL, ER_DD_VERSION_FOUND, d->get_actual_dd_version(thd)); return false; }
啟動時各個數據字典表的根頁面信息是從 mysql.dd_properties表中獲取的,通過該頁面可以訪問對應表的所有數據。
mysql.dd_properties表的根頁面是固定的,並且它裡面保存了數組字典表本身的元數據。相關函數:dd::get_se_private_data()。
小結
MySQL 8.0新設計實現的數據字典,解決了之前版本的數據字典冗餘,DDL原子性、crash safe等問題。通過對數據字典的初始化流程,以及數據字典正常重啟時載入流程的梳理,希望讀者對新數據字典的實現和運行有一個更深入的瞭解。
後續會繼續探究MySQL 8.0數據字典版本升級的內容,敬請期待。
更多技術專欄內容:https://bbs.huaweicloud.com/blogs/427078