MySQL 深潜 - 一文详解 MySQL Data Dictionary

网友投稿 1407 2023-06-02

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 在 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 也是代码实现中 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、引擎、选项、范围值、父子分区等。

因此获取到一个表的 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' 直接访问查看。比如:

通过以上 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()       |--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    };  };

四、多级缓存

两级缓存的底层实现很统一,都是基于 hash map 的,目前的实现是 std::map。Local_multi_map 和 Shared_multi_map都是派生于 Multi_map_base。

template   class Multi_map_base {   private:    Element_map> m_rev_map;  // Reverse element map.    Element_map>        m_id_map;  // Id map instance.    Element_map>        m_name_map;  // Name map instance.    Element_map>        m_aux_map;  // Aux map instance.  };  template   class Element_map {   public:    typedef std::map,                     Malloc_allocator>>        Element_map_type;  // Real map type.   private:    Element_map_type m_map;  // The real map instance.    std::set,             Malloc_allocator>        m_missed;  // Cache misses being handled.  };

之所以叫 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.局部缓存

2.共享缓存

共享缓存是 Server 全局唯一的,使用单例 Shared_dictionary_cache 来实现。与上述局部缓存中 Object_registry 相似,Shared_dictionary_cache 也需要包含针对各种类型对象的缓存。与 Multi_map_base 实现根据 key 类型自动选取对应 hash map 的模版函数相似,Object_registry 和 Shared_dictionary_cache 也都实现了根据访问对象的类型选择对应缓存的 m_map() 函数,能够很大程度上简化函数调用。

class Shared_dictionary_cache {    Shared_multi_map m_abstract_table_map;    Shared_multi_map m_charset_map;    Shared_multi_map m_collation_map;    Shared_multi_map m_column_stat_map;    Shared_multi_map m_event_map;    Shared_multi_map m_resource_group_map;    Shared_multi_map m_routine_map;    Shared_multi_map m_schema_map;    Shared_multi_map m_spatial_reference_system_map;    Shared_multi_map m_tablespace_map;  };   template   class Shared_multi_map : public Multi_map_base {  private:    static const size_t initial_capacity = 256;    mysql_mutex_t m_lock;         // Single mutex to lock the map.    mysql_cond_t m_miss_handled;  // Broadcast a miss being handled.    Free_list> m_free_list;  // Free list.    std::vector *>        m_element_pool;  // Pool of allocated elements.   size_t m_capacity;   // Total capacity, i.e., if the                         // number of elements exceeds this                         // limit, shrink the free list.  }

与局部缓存可以无锁访问 hash map 不同,共享缓存在获取 / 释放 DD cache object 时都需要加锁来完成引用计数的调整和防止访问过程中被 destroy 掉。

3.缓存获取过程

用户通过 client 调用元数据对象获取函数,传入元数据的 name 字符串,然后构建出对应的 name key,通过 key 去缓存中获取元数据对象。获取的整体过程就是一级局部缓存 -> 二级共享缓存 -> 存储引擎。

Cache miss

// Get a wrapper element from the map handling the given key type.  template   template   bool Shared_multi_map::get(const K &key, Cache_element **element) {    Autolocker lock(this);    *element = use_if_present(key);    if (*element) return false;    // Is the element already missed?    if (m_map()->is_missed(key)) {      while (m_map()->is_missed(key))        mysql_cond_wait(&m_miss_handled, &m_lock);      *element = use_if_present(key);      // Here, we return only if element is non-null. An absent element      // does not mean that the object does not exist, it might have been      // evicted after the thread handling the first cache miss added      // it to the cache, before this waiting thread was alerted. Thus,      // we need to handle this situation as a cache miss if the element      // is absent.      if (*element) return false;    }    // Mark the key as being missed.    m_map()->set_missed(key);    return true;  }

由于开表访问 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,他们之间通过一个简单的链表指针连接起来。在函数返回时将本层需要 release 的对象 release 掉,需要返回给上层使用的 DD cache 对象交给上层的 Auto_releaser 来负责。通过 transfer_release() 可以在不同层次的 Auto_releaser 对象间转移需要 release 的对象,可以灵活的指定不再需要 DD cache 对象的层次。

六、应用举例:inplace DDL 过程中对 DD 的操作

在 MySQL inplace DDL 执行过程中,会获取当前表定义的 DD cache 对象,然后根据实际的 DDL 操作内容构造出新对应的 DD 对象。然后依次调用 client 的接口完成对当前表定义的删除和新表定义的存储。

在 drop() 过程中,会将当前表定义的 DD cache 对象对应的数据从存储引擎中删除,然后从共享缓存中移除(这要求当前对象的引用计数仅为1,即只有当前线程使用),之后加入到 dropped 局部缓存中。

在这个过程中,由于 MDL(metadata lock) 的存在,不会有其他的线程尝试访问正在变更对象的 DD object,所以可以安全的对 Shared_dictionary_cache 进行操作。当 DDL 操作结束(提交或回滚),释放 EXCLUSIVE 锁之后,新的线程就可以重新从存储引擎上加载新的表定义。

七、总结

MySQL data dictionary 解决了背景所述旧架构中的诸多问题,使元数据的访问更加安全,存储和管理成本更低。架构实现非常的精巧,通过大量的模版类实现使得代码能够最大程度上被复用。多层缓存的实现也能显著提升访问效率。通过 client 简洁的接口,让 Server 层和存储层能在任何地方方便的访问元数据。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:金融行业数据如何管理?金融行业数据治理“合法合规”与“价值创造”如何走好平衡木?
下一篇:切记!MySQL 中 ORDER BY 与 IMIT 不要一起用,有大坑
相关文章