MySQL 深潜 - 一文详解 MySQL Data Dictionary
簡介:?在 MySQL 8.0 之前,Server 層和存儲引擎(比如 InnoDB)會各自保留一份元數據(schema name, table definition 等),不僅在信息存儲上有著重復冗余,而且可能存在兩者之間存儲的元數據不同步的現象。不同存儲引擎之間(比如 InnoDB 和 MyISAM)有著不同的元數據存儲形式和位置(.FRM, .PAR, .OPT, .TRN and .TRG files),造成了元數據無法統一管理。此外,將元數據存放在不支持事務的表和文件中,使得 DDL 變更不會是原子的,crash recovery 也會成為一個問題。
作者 | 泊歌
來源 | 阿里技術公眾號
一 背景
在 MySQL 8.0 之前,Server 層和存儲引擎(比如 InnoDB)會各自保留一份元數據(schema name, table definition 等),不僅在信息存儲上有著重復冗余,而且可能存在兩者之間存儲的元數據不同步的現象。不同存儲引擎之間(比如 InnoDB 和 MyISAM)有著不同的元數據存儲形式和位置(.FRM, .PAR, .OPT, .TRN and .TRG files),造成了元數據無法統一管理。此外,將元數據存放在不支持事務的表和文件中,使得 DDL 變更不會是原子的,crash recovery 也會成為一個問題。
為了解決上述問題,MySQL 在 8.0 中引入了 data dictionary 來進行 Server 層和不同引擎間統一的元數據管理,這些元數據都存儲在 InnoDB 引擎的表中,自然的支持原子性,且 Server 層和引擎層共享一份元數據,不再存在不同步的問題。
二 整體架構
data dictionary 提供了統一的 client API 供 Server 層和引擎層使用,包含對元數據訪問的 acquire() / drop() / store() / update() 基本操作。底層實現了對 InnoDB 引擎存放的數據字典表的讀寫操作,包含開表(open table)、構造主鍵、主鍵查找等過程。client 和底層存儲之間通過兩級緩存來加速對元數據對象的內存訪問,兩級緩存都是基于 hash map 實現的,一層緩存是 local 的,由每個 client(每個線程對應一個 client)獨享;二級緩存是 share 的,為所有線程共享的全局緩存。下面我將對 data dictionary 的數據結構和實現架構做重點介紹,也會分享一個支持原子的 DDL 在 data dictionary 層面的實現過程。
三 metadata 在內存和引擎層面的表示
data dictionary (簡稱DD)中的數據結構是完全按照多態、接口/實現的形式來組織的,接口通過純虛類來實現(比如表示一個表的 Table),其實現類(Table_impl)為接口類的名字加 _impl 后綴。下面以 Table_impl 為例介紹一個表的元數據對象在 DD cache 中的表示。
1 Table_impl
Table_impl 類中包含一個表相關的元數據屬性定義,比如下列最基本引擎類型、comment、分區類型、分區表達式等。
class Table_impl : public Abstract_table_impl, virtual public Table {// Fields.Object_id m_se_private_id;String_type m_engine;String_type m_comment;// - Partitioning related fields.enum_partition_type m_partition_type;String_type m_partition_expression;String_type m_partition_expression_utf8;enum_default_partitioning m_default_partitioning;// References to tightly-coupled objects.Index_collection m_indexes;Foreign_key_collection m_foreign_keys;Foreign_key_parent_collection m_foreign_key_parents;Partition_collection m_partitions;Partition_leaf_vector m_leaf_partitions;Trigger_collection m_triggers;Check_constraint_collection m_check_constraints; };Table_impl 也是代碼實現中 client 最常訪問的內存結構,開發者想要增加新的屬性,直接在這個類中添加和初始化即可,但是僅僅如此不會自動將該屬性持久化到存儲引擎中。除了上述簡單屬性之外,還包括與一個表相關的復雜屬性,比如列信息、索引信息、分區信息等,這些復雜屬性都是存在其他的 DD 表中,在內存 cache 中也都會集成到 Table_impl 對象里。
從 Abstract_table_impl 繼承來的 Collection m_columns 就表示表的所有列集合,集合中的每一個對象 Column_impl 表示該列的元信息,包括數值類型、是否為 NULL、是否自增、默認值等。同時也包含指向 Abstract_table_impl 的指針,將該列與其對應的表聯系起來。
class Column_impl : public Entity_object_impl, public Column {// Fields.enum_column_types m_type;bool m_is_nullable;bool m_is_zerofill;bool m_is_unsigned;bool m_is_auto_increment;bool m_is_virtual;bool m_default_value_null;String_type m_default_value;// References to tightly-coupled objects.Abstract_table_impl *m_table; };此外 Table_impl 中也包含所有分區的元信息集合 Collection m_partitions,存放每個分區的 id、引擎、選項、范圍值、父子分區等。
class Partition_impl : public Entity_object_impl, public Partition {// Fields.Object_id m_parent_partition_id;uint m_number;Object_id m_se_private_id;String_type m_description_utf8;String_type m_engine;String_type m_comment;Properties_impl m_options;Properties_impl m_se_private_data;// References to tightly-coupled objects.Table_impl *m_table;const Partition *m_parent;Partition_values m_values;Partition_indexes m_indexes;Table::Partition_collection m_sub_partitions; };因此獲取到一個表的 Table_impl,我們就可以獲取到與這個表相關聯的所有元信息。
2 Table_impl 是如何持久化存儲和訪問的
DD cache 中的元信息都是在 DD tables 中讀取和存儲的,每個表存放一類元信息的基本屬性字段,比如 tables、columns、indexes等,他們之間通過主外鍵關聯連接起來,組成 Table_impl 的全部元信息。DD tables 存放在 mysql 的表空間中,在 release 版本對用戶隱藏,只能通過 INFORMATION SCHEMA 的部分視圖查看;在 debug 版本可通過設置 SET debug='+d,skip_dd_table_access_check' 直接訪問查看。比如:
root@localhost:test 8.0.18-debug> SHOW CREATE TABLE mysql.tables\G *************************< strong> 1. row < /strong>*************************Table: tables Create Table: CREATE TABLE `tables` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`schema_id` bigint(20) unsigned NOT NULL,`name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,`type` enum('BASE TABLE','VIEW','SYSTEM VIEW') COLLATE utf8_bin NOT NULL,`engine` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`mysql_version_id` int(10) unsigned NOT NULL,`row_format` enum('Fixed','Dynamic','Compressed','Redundant','Compact','Paged') COLLATE utf8_bin DEFAULT NULL,`collation_id` bigint(20) unsigned DEFAULT NULL,`comment` varchar(2048) COLLATE utf8_bin NOT NULL,`hidden` enum('Visible','System','SE','DDL') COLLATE utf8_bin NOT NULL,`options` mediumtext COLLATE utf8_bin,`se_private_data` mediumtext COLLATE utf8_bin,`se_private_id` bigint(20) unsigned DEFAULT NULL,`tablespace_id` bigint(20) unsigned DEFAULT NULL,`partition_type` enum('HASH','KEY_51','KEY_55','LINEAR_HASH','LINEAR_KEY_51','LINEAR_KEY_55','RANGE','LIST','RANGE_COLUMNS','LIST_COLUMNS','AUTO','AUTO_LINEAR') COLLATE utf8_bin DEFAULT NULL,`partition_expression` varchar(2048) COLLATE utf8_bin DEFAULT NULL,`partition_expression_utf8` varchar(2048) COLLATE utf8_bin DEFAULT NULL,`default_partitioning` enum('NO','YES','NUMBER') COLLATE utf8_bin DEFAULT NULL,`subpartition_type` enum('HASH','KEY_51','KEY_55','LINEAR_HASH','LINEAR_KEY_51','LINEAR_KEY_55') COLLATE utf8_bin DEFAULT NULL,`subpartition_expression` varchar(2048) COLLATE utf8_bin DEFAULT NULL,`subpartition_expression_utf8` varchar(2048) COLLATE utf8_bin DEFAULT NULL,`default_subpartitioning` enum('NO','YES','NUMBER') COLLATE utf8_bin DEFAULT NULL,`created` timestamp NOT NULL,`last_altered` timestamp NOT NULL,`view_definition` longblob,`view_definition_utf8` longtext COLLATE utf8_bin,`view_check_option` enum('NONE','LOCAL','CASCADED') COLLATE utf8_bin DEFAULT NULL,`view_is_updatable` enum('NO','YES') COLLATE utf8_bin DEFAULT NULL,`view_algorithm` enum('UNDEFINED','TEMPTABLE','MERGE') COLLATE utf8_bin DEFAULT NULL,`view_security_type` enum('DEFAULT','INVOKER','DEFINER') COLLATE utf8_bin DEFAULT NULL,`view_definer` varchar(288) COLLATE utf8_bin DEFAULT NULL,`view_client_collation_id` bigint(20) unsigned DEFAULT NULL,`view_connection_collation_id` bigint(20) unsigned DEFAULT NULL,`view_column_names` longtext COLLATE utf8_bin,`last_checked_for_upgrade_version_id` int(10) unsigned NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `schema_id` (`schema_id`,`name`),UNIQUE KEY `engine` (`engine`,`se_private_id`),KEY `engine_2` (`engine`),KEY `collation_id` (`collation_id`),KEY `tablespace_id` (`tablespace_id`),KEY `type` (`type`),KEY `view_client_collation_id` (`view_client_collation_id`),KEY `view_connection_collation_id` (`view_connection_collation_id`),CONSTRAINT `tables_ibfk_1` FOREIGN KEY (`schema_id`) REFERENCES `schemata` (`id`),CONSTRAINT `tables_ibfk_2` FOREIGN KEY (`collation_id`) REFERENCES `collations` (`id`),CONSTRAINT `tables_ibfk_3` FOREIGN KEY (`tablespace_id`) REFERENCES `tablespaces` (`id`),CONSTRAINT `tables_ibfk_4` FOREIGN KEY (`view_client_collation_id`) REFERENCES `collations` (`id`),CONSTRAINT `tables_ibfk_5` FOREIGN KEY (`view_connection_collation_id`) REFERENCES `collations` (`id`) ) /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB AUTO_INCREMENT=549 DEFAULT CHARSET=utf8 COLLATE=utf8_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC 1 row in set (0.00 sec)通過以上 mysql.tables 的表定義可以獲得存儲引擎中實際存儲的元信息字段。DD tables 包括 tables、schemata、columns、column_type_elements、indexes、index_column_usage、foreign_keys、foreign_key_column_usage、table_partitions、table_partition_values、index_partitions、triggers、check_constraints、view_table_usage、view_routine_usage 等。
Storage_adapter 是訪問持久存儲引擎的處理類,包括 get() / drop() / store() 等接口。當初次獲取一個表的元信息時,會調用 Storage_adapter::get() 接口,處理過程如下:
Storage_adapter::get()// 根據訪問對象類型,將依賴的 DD tables 加入到 open table list 中|--Open_dictionary_tables_ctx::register_tables< T>() |--Table_impl::register_tables()|--Open_dictionary_tables_ctx::open_tables() // 調用 Server 層接口打開所有表|--Raw_table::find_record() // 直接調用 handler 接口根據傳入的 key(比如表名)查找記錄|--handler::ha_index_read_idx_map() // index read// 從讀取到的 record 中解析出對應屬性,調用 field[field_no]->val_xx() 函數|--Table_impl::restore_attributes()// 通過調用 restore_children() 函數從與該對象關聯的其他 DD 表中根據主外鍵讀取完整的元數據定義|--Table_impl::restore_children() |--返回完整的 DD cache 對象上述在獲取列和屬性的對應關系時,根據的是 Tables 對象的枚舉類型下標,按順序包含了該類型 DD 表中的所有列,與上述表定義是一一對應的。因此如果我們需要新增 DD 表中存儲的列時,也需要往下面枚舉類型定義中加入對應的列,并且在 Table_impl::restore_attributes() / Table_impl::store_attributes() 函數中添加對新增列的讀取和存儲操作。
class Tables : public Entity_object_table_impl {enum enum_fields {FIELD_ID,FIELD_SCHEMA_ID,FIELD_NAME,FIELD_TYPE,FIELD_ENGINE,FIELD_MYSQL_VERSION_ID,FIELD_ROW_FORMAT,FIELD_COLLATION_ID,FIELD_COMMENT,FIELD_HIDDEN,FIELD_OPTIONS,FIELD_SE_PRIVATE_DATA,FIELD_SE_PRIVATE_ID,FIELD_TABLESPACE_ID,FIELD_PARTITION_TYPE,FIELD_PARTITION_EXPRESSION,FIELD_PARTITION_EXPRESSION_UTF8,FIELD_DEFAULT_PARTITIONING,FIELD_SUBPARTITION_TYPE,FIELD_SUBPARTITION_EXPRESSION,FIELD_SUBPARTITION_EXPRESSION_UTF8,FIELD_DEFAULT_SUBPARTITIONING,FIELD_CREATED,FIELD_LAST_ALTERED,FIELD_VIEW_DEFINITION,FIELD_VIEW_DEFINITION_UTF8,FIELD_VIEW_CHECK_OPTION,FIELD_VIEW_IS_UPDATABLE,FIELD_VIEW_ALGORITHM,FIELD_VIEW_SECURITY_TYPE,FIELD_VIEW_DEFINER,FIELD_VIEW_CLIENT_COLLATION_ID,FIELD_VIEW_CONNECTION_COLLATION_ID,FIELD_VIEW_COLUMN_NAMES,FIELD_LAST_CHECKED_FOR_UPGRADE_VERSION_ID,NUMBER_OF_FIELDS // Always keep this entry at the end of the enum}; };四 多級緩存
為了避免每次對元數據對象的訪問都需要去持久存儲中讀取多個表的數據,使生成的元數據內存對象能夠復用,data dictionary 實現了兩級緩存的架構,第一級是 client local 獨享的,核心數據結構為 Local_multi_map,用于加速在當前線程中對于相同對象的重復訪問,同時在當前線程涉及對 DD 對象的修改(DDL)時管理 committed、uncommitted、dropped 幾種狀態的對象。第二級就是比較常見的多線程共享的緩存,核心數據結構為 Shared_multi_map,包含著所有線程都可以訪問到其中的對象,所以會做并發控制的處理。
兩級緩存的底層實現很統一,都是基于 hash map 的,目前的實現是 std::map。Local_multi_map 和 Shared_multi_map都是派生于 Multi_map_base。
之所以叫 Multi_map_base,是因為其中包含了多個 hash map,適合用戶根據不同類型的 key 來獲取緩存對象,比如 id、name、DD cache 本身等。Element_map 就是對 std::map 的一個封裝,key 為前述幾種類型之一,value 為 DD cache 對象指針的一個封裝 Cache_element,封裝了對象本身和引用計數。
Multi_map_base 對象實現了豐富的 m_map() 模板函數,可以很方便的根據 key 的類型不同選擇到對應的 hash map。
Shared_multi_map 與 Local_multi_map 的不同在于,Shared_multi_map 還引入了一組 latch 與 condition variable 用于并發訪問中的線程同步與 cache miss 的處理。同時對 Cache_element 對象做了內存管理和復用的相關能力。
1 局部緩存
一級緩存位于每個 Dictionary_client (每個 client 與線程 THD 一一對應)內部,由不同狀態(committed、uncommitted、dropped)的 Object_registry 組成。每個 Object_registry 由不同元數據類型的 Local_multi_map 組成,用于管理不同類型的對象(比如表、schema、字符集、統計數據、Event 等)緩存。
其中 committed 狀態的 registry 就是我們訪問數據庫中已經存在的對象時,將其 DD cache object 存放在局部緩存中的位置。uncommitted 和 dropped 狀態的存在,主要用于當前連接執行的是一條 DDL 語句,在執行過程中會將要 drop 的舊表對應的 DD object 存放在 dropped 的 registry 中,將還未提交的新表定義對應的 DD object 存放在 uncommitted 的 registry 中,用于執行狀態的區分。
2 共享緩存
共享緩存是 Server 全局唯一的,使用單例 Shared_dictionary_cache 來實現。與上述局部緩存中 Object_registry 相似,Shared_dictionary_cache 也需要包含針對各種類型對象的緩存。與 Multi_map_base 實現根據 key 類型自動選取對應 hash map 的模版函數相似,Object_registry 和 Shared_dictionary_cache 也都實現了根據訪問對象的類型選擇對應緩存的 m_map() 函數,能夠很大程度上簡化函數調用。
與局部緩存可以無鎖訪問 hash map 不同,共享緩存在獲取 / 釋放 DD cache object 時都需要加鎖來完成引用計數的調整和防止訪問過程中被 destroy 掉。
3 緩存獲取過程
用戶通過 client 調用元數據對象獲取函數,傳入元數據的 name 字符串,然后構建出對應的 name key,通過 key 去緩存中獲取元數據對象。獲取的整體過程就是一級局部緩存 -> 二級共享緩存 -> 存儲引擎。
// Get a dictionary object. template < typename K, typename T> bool Dictionary_client::acquire(const K &key, const T **object,bool *local_committed,bool *local_uncommitted) {// Lookup in registry of uncommitted objectsT *uncommitted_object = nullptr;bool dropped = false;acquire_uncommitted(key, &uncommitted_object, &dropped);...// Lookup in the registry of committed objects.Cache_element< T> *element = NULL;m_registry_committed.get(key, &element);...// Get the object from the shared cache.if (Shared_dictionary_cache::instance()->get(m_thd, key, &element)) {DBUG_ASSERT(m_thd->is_system_thread() || m_thd->killed ||m_thd->is_error());return true;} }在一級局部緩存中獲取時,會優先去 uncommitted 和 dropped 的 registry 獲取,因為這兩者是最新的修改,同時判斷獲取對象是否已經被 dropped。之后再會去 committed 的 registry 獲取,如果獲取到就直接返回,反之則去二級共享緩存中嘗試獲取。
Cache miss
共享緩存的獲取過程在 Shared_multi_map::get() 中實現。就是加鎖后直接的 hash map 查找,如果存在則給引用計數遞增后返回;如果不存在,就會進入到 cache miss 的處理過程,調用上面介紹的存儲引擎的接口 Storage_adapter::get() 從 DD tables 中讀取,創建出來后依次加入共享緩存和局部緩存 committed registry 中。
由于開表訪問 DD tables,構建 DD cache object 的過程相對耗時,不會一直給 Shared_multi_map 加鎖,因此需要對并發訪問的 client 做并發控制。DD 的實現方法是第一個訪問的 client 會將 cache miss 的 key 加入到 Shared_multi_map的 m_missed 集合中,這個集合包含著現在所有正在讀取元數據的對象 key 值。之后訪問的 client 看到目標 key 值在 m_missed 集合中就會進入等待。
當第一個 client 獲取到完整的 DD cache object,加入到共享緩存之后,移除 m_missed 集合中對應的 key,并通過廣播的方式通知之前等待的線程重新在共享緩存中獲取。
五 Auto_releaser
Auto_releaser 是一個 RAII 類,基本上在使用 client 訪問 DD cache 前都會做一個封裝,保證在整個 Auto_releaser 對象存在的作用域內,所獲取到的 DD cache 對象都會在局部緩存中存在不釋放。Auto_releaser 包含需要 release 的對象 registry,通過 auto_release() 函數收集著當前 client 從共享緩存中獲取到的 DD cache 對象,在超出其作用域進行析構時自動 release 對象,從局部緩存 committed 的 registry 中移除對象,并且在共享緩存中的引用計數遞減。
在嵌套函數調用過程中,可能在每一層都會有自己的 Auto_releaser,他們之間通過一個簡單的鏈表指針連接起來。在函數返回時將本層需要 release 的對象 release 掉,需要返回給上層使用的 DD cache 對象交給上層的 Auto_releaser 來負責。通過 transfer_release() 可以在不同層次的 Auto_releaser 對象間轉移需要 release 的對象,可以靈活的指定不再需要 DD cache 對象的層次。
六 應用舉例:inplace DDL 過程中對 DD 的操作
在 MySQL inplace DDL 執行過程中,會獲取當前表定義的 DD cache 對象,然后根據實際的 DDL 操作內容構造出新對應的 DD 對象。然后依次調用 client 的接口完成對當前表定義的刪除和新表定義的存儲。
{ if (thd->dd_client()->drop(table_def)) goto cleanup2;table_def = nullptr;DEBUG_SYNC_C("alter_table_after_dd_client_drop");// Reset check constraint's mode.reset_check_constraints_alter_mode(altered_table_def);if ((db_type->flags & HTON_SUPPORTS_ATOMIC_DDL)) {/*For engines supporting atomic DDL we have delayed storing newtable definition in the data-dictionary so far in order to avoidconflicts between old and new definitions on foreign key names.Since the old table definition is gone we can safely store newdefinition now.*/if (thd->dd_client()->store(altered_table_def)) goto cleanup2;} }.../*If the SE failed to commit the transaction, we must rollback themodified dictionary objects to make sure the DD cache, the DDtables and the state in the SE stay in sync. */ if (res)thd->dd_client()->rollback_modified_objects(); elsethd->dd_client()->commit_modified_objects();在 drop() 過程中,會將當前表定義的 DD cache 對象對應的數據從存儲引擎中刪除,然后從共享緩存中移除(這要求當前對象的引用計數僅為1,即只有當前線程使用),之后加入到 dropped 局部緩存中。
在 store() 過程中,會將新的表定義寫入存儲引擎,并且將對應的 DD cache 對象加入 uncommitted 緩存中。
在事務提交或者回滾后,client 將局部緩存中的 dropped 和 uncommitted registry 清除。由于 InnoDB 引擎支持事務,持久存儲層面的數據會通過存儲引擎的接口提交或回滾,不需要 client 額外操作。
在這個過程中,由于 MDL(metadata lock) 的存在,不會有其他的線程嘗試訪問正在變更對象的 DD object,所以可以安全的對 Shared_dictionary_cache 進行操作。當 DDL 操作結束(提交或回滾),釋放 EXCLUSIVE 鎖之后,新的線程就可以重新從存儲引擎上加載新的表定義。
七 總結
MySQL data dictionary 解決了背景所述舊架構中的諸多問題,使元數據的訪問更加安全,存儲和管理成本更低。架構實現非常的精巧,通過大量的模版類實現使得代碼能夠最大程度上被復用。多層緩存的實現也能顯著提升訪問效率。通過 client 簡潔的接口,讓 Server 層和存儲層能在任何地方方便的訪問元數據。
原文鏈接
本文為阿里云原創內容,未經允許不得轉載。?
總結
以上是生活随笔為你收集整理的MySQL 深潜 - 一文详解 MySQL Data Dictionary的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 庖丁解InnoDB之REDO LOG
- 下一篇: 作业帮云原生降本增效实践之路