天天看點

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

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 深潛 - 一文詳解 MySQL Data Dictionary

為了解決上述問題,MySQL 在 8.0 中引入了 data dictionary 來進行 Server 層和不同引擎間統一的中繼資料管理,這些中繼資料都存儲在 InnoDB 引擎的表中,自然的支援原子性,且 Server 層和引擎層共享一份中繼資料,不再存在不同步的問題。

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

二 整體架構

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

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。

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

之是以叫 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 等)緩存。

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

其中 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() 函數,能夠很大程度上簡化函數調用。

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

與局部緩存可以無鎖通路 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 objects
  T *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 中。

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

由于開表通路 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 中移除對象,并且在共享緩存中的引用計數遞減。

MySQL 深潛 - 一文詳解 MySQL Data Dictionary

在嵌套函數調用過程中,可能在每一層都會有自己的 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 new
      table definition in the data-dictionary so far in order to avoid
      conflicts between old and new definitions on foreign key names.
      Since the old table definition is gone we can safely store new
      definition now.
    */
    if (thd->dd_client()->store(altered_table_def)) goto cleanup2;
  }
}

...

/*
  If the SE failed to commit the transaction, we must rollback the
  modified dictionary objects to make sure the DD cache, the DD
  tables and the state in the SE stay in sync.
*/
if (res)
  thd->dd_client()->rollback_modified_objects();
else
  thd->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 層和存儲層能在任何地方友善的通路中繼資料。

參考

[1] MySQL8.0DataDictionary:BackgroundandMotivation

http://mysqlserverteam.com/mysql-8-0-data-dictionary-background-and-motivation/

[2] MySQL 8.0: Data Dictionary Architecture and Design

http://mysqlserverteam.com/mysql-8-0-data-dictionary-architecture-and-design/

[3] Source code mysql / mysql-server 8.0.18

https://github.com/mysql/mysql-server/tree/mysql-8.0.18

低代碼征文召集令!重磅來襲

低代碼開發者征文召集令!參與低代碼話題相關投稿,談談“你對低代碼的了解”,“利用低代碼工具真的實作降本增效嗎”等話題。活動準備了Air Pods Pro,機械鍵盤,移動硬碟,阿裡雲定制書包等精美禮品等你來領!快來參與吧!

點選這裡

,即可參加活動~