美文网首页一些收藏
MySQL:8.0全新的字典缓存(代替5.7 frm文件)

MySQL:8.0全新的字典缓存(代替5.7 frm文件)

作者: 重庆八怪 | 来源:发表于2022-09-27 12:10 被阅读0次

    水平有限仅供参考,仅供参考。


    一、综述

    在MySQL8.0中我们没有了frm文件,取而带之的是全新的字段缓存的设计和多个持久化的字典表,这部分不仅为原子性DDL提供了基础,而且减少打开物理frm文件的开销。
    但是原先的table/table_share的缓存依旧架设在前面。因此看起来在获取表的字典数据的时候就依次为:

    1. table_cache (大概率就命中了,参数相关,分多个实例,每个session只能用一个实例)
    2. table_define_cache(获取table share,命中率高,参数相关)
    3. Dictionary_client(字典元素)
    4. Shared_dictionary_cache(字典元素,命中率高,最大可缓存max connections个数的表字典信息)
    5. 持久化的表

    而Dictionary_client和Shared_dictionary_cache和持久化的表就代替了原先的frm文件。这里先说一下flush tables语句,从debug来看并不影响 3和4的缓存。

    二、Dictionary_client

    Dictionary_client是一个session(THD)相关的,也就是session自己的,其中在Dictionary_client内包含一个重要的元素叫做Object_registry,其中包含了各种字典类型的map,如下:

      std::unique_ptr<Local_multi_map<Abstract_table>> m_abstract_table_map;
      std::unique_ptr<Local_multi_map<Charset>> m_charset_map;
      std::unique_ptr<Local_multi_map<Collation>> m_collation_map;
      std::unique_ptr<Local_multi_map<Column_statistics>> m_column_statistics_map;
      std::unique_ptr<Local_multi_map<Event>> m_event_map;
      std::unique_ptr<Local_multi_map<Resource_group>> m_resource_group_map;
      std::unique_ptr<Local_multi_map<Routine>> m_routine_map;
      std::unique_ptr<Local_multi_map<Schema>> m_schema_map;
      std::unique_ptr<Local_multi_map<Spatial_reference_system>>
    

    每种类型的元数据都缓存在自己的map中,比如我们常说的Schema和Abstract_table,这里需要注意的是他们都是父类,比如


    image.png

    当然table还会更加复杂一些。具体的可以从父类Entity_object开始进行探索。但是需要注意的是这里是一个unique_ptr类型的智能指针,也就是说不能共享。

    而Local_multi_map来自Multi_map_base,而Multi_map_base又包含了如下4种map,除了m_aux_map,其他3种如下:

      Element_map<const T *, Cache_element<T>> m_rev_map;  // Reverse element map.
      Element_map<typename T::Id_key, Cache_element<T>>
          m_id_map;  // Id map instance.
      Element_map<typename T::Name_key, Cache_element<T>>
          m_name_map;  // Name map instance.
    

    其实从multi名字也可以看出来是多个map。

    Element_map实际上就是一个std::map的封装,我们可以看到3种根据是指针地址/Id_key/Name_key 进行的分别map,主要应对多种查询方式。如果以实例化的dd::Table为例子,Id_key/Name_key其定义如下:

      typedef Primary_id_key Id_key; (表中的主键?)
      typedef Item_name_key Name_key;(字符串名字)
    

    而Cache_element(dd::cache::Cache_element)就是实际元素的封装拉,也是map的value值,其中包含一些元素如下:

      const T *m_object;   // Pointer to the actual object.
      uint m_ref_counter;  // Number of concurrent object usages.
      Key_wrapper<typename T::Id_key> m_id_key;      // The id key for the object.
      Key_wrapper<typename T::Name_key> m_name_key;  // The name key for the object.
      Key_wrapper<typename T::Aux_key> m_aux_key;    // The aux key for the object.
    

    其中const T *m_object就是实际指向对象的指针了,比如一个dd::Table的元数据,也就是实际的一个字典元素,而m_ref_counter在后面的Shared_dictionary_cache会用到,主要是一个LRU链表的会使用到,对于超过最大容量得做淘汰,后面再说。

    最后Dictionary_client实际上提供了3个Object_registry元素如下

    • m_registry_uncommitted
      加入时机: dd::cache::Dictionary_client::update
      比如实例化的函数: dd::cache::Dictionary_client::update<dd::Table>
    • m_registry_committed
      加入时机: dd::cache::Dictionary_client::remove_uncommitted_objects
      比如实例化的函数:
      dd::cache::Dictionary_client::remove_uncommitted_objects<dd::Table>
    • m_registry_dropped
      加入时机: dd::cache::Dictionary_client::register_dropped_object
      比如实例化的函数: dd::cache::Dictionary_client::register_dropped_object<dd::Table>

    实际上这部分主要和DDL操作进行的状态转换有关,这里先不考虑了,因为原子化DDL没有学习过。但是从流程来看,一般select语句会加入到m_registry_committed中,并且查找也会在里面查找。
    那么总结一下,这里面包含3个Object_registry元素,每个元素包含多个Local_multi_map,而每个Local_multi_map是Multi_map_base的继承,每个Multi_map_base包含了4个map,其中3个常用,分别是主键/名字/元素的指针 为key,元素就是对应的Cache_element。Cache_element原则也是一个元素的指针。

    image.png

    三、Shared_dictionary_cache

    Shared_dictionary_cache是全局的,使用的是单例模式,这部分可以在dd::cache::Shared_dictionary_cache::instance函数中找到他的static变量,如下:


    image.png

    实际上Shared_dictionary_cache和Dictionary_client类似,只是没有3个Object_registry,取而代之是基于Multi_map_base实现的Shared_multi_map,并且也是包含各个字典类型的map,但是在Shared_multi_map实现中加入了LRU链表的方式,因为Shared_dictionary_cache会缓存较多的字段元素(比如一个表的字典)。

    在Shared_dictionary_cache初始化(dd::cache::Shared_dictionary_cache::init)的时候会根据各自最大容量限制,如下:

    void Shared_dictionary_cache::init() {
      instance()->m_map<Collation>()->set_capacity(collation_capacity);
      instance()->m_map<Charset>()->set_capacity(charset_capacity);
    
      // Set capacity to have room for all connections to leave an element
      // unused in the cache to avoid frequent cache misses while e.g.
      // opening a table.
      instance()->m_map<Abstract_table>()->set_capacity(max_connections);
      instance()->m_map<Event>()->set_capacity(event_capacity);
      instance()->m_map<Routine>()->set_capacity(stored_program_def_size);
      instance()->m_map<Schema>()->set_capacity(schema_def_size);
      instance()->m_map<Column_statistics>()->set_capacity(
          column_statistics_capacity);
      instance()->m_map<Spatial_reference_system>()->set_capacity(
          spatial_reference_system_capacity);
      instance()->m_map<Tablespace>()->set_capacity(tablespace_def_size);
      instance()->m_map<Resource_group>()->set_capacity(resource_group_capacity);
    }
    

    其中大部分都在Shared_dictionary_cache的定义中,硬编码如下:

      static const size_t collation_capacity = 256;
      static const size_t column_statistics_capacity = 32;
      static const size_t charset_capacity = 64;
      static const size_t event_capacity = 256;
      static const size_t spatial_reference_system_capacity = 256;
      static const size_t resource_group_capacity = 32;
    

    但是我们发现有如下不同

    • Abstract_table map:最大值和参数max_connections有关这也是我们最关注的,表的字典信息的缓存。
    • Schema map:最大值和参数schema_definition_cache有关,默认256
    • Tablespace map:最大值和参数tablespace_definition_cache有关,默认256
    • Routine map:最大值和参数stored_program_definition_cache有关,默认256

    实际上当我们的table share失效过后,一般用到的都是这里的map,因此依赖LRU进行缓存是非常有必要的。

    而在Dictionary_client的RAII类析构的时候会自动调用释放,释放的时候调用函数,
    template <typename T>
    size_t Dictionary_client::release(Object_registry *registry)
    如下:

        // Release the element from the shared cache.
        Shared_dictionary_cache::instance()->release(element);//在share中去除
    
    这里就是调用的
      template <typename T>
      void release(Cache_element<T> *e) {
        m_map<T>()->release(e);
      }
    
    实际上就是,Shared_multi_map的release方法,关键如下:
    
    Shared_multi_map<T>::release
      // Release the element.
      element->release(); //计数器 -1
    
      // If the element is not used, add it to the free list.
      if (element->usage() == 0) {
        m_free_list.add_last(element);
        rectify_free_list(&lock); //放到free list
      }
    
    

    实际上就是上面说的Cache_element中m_ref_counter的作用。
    后面进行DEBUG发现,确实大部分访问过的表的字段都会存在于Abstract_table map中,这个比较简单,只要拿到static指针的地址去访问就可以了如下:

    • p (*((dd::cache::Shared_dictionary_cache *) 0x84be3c0)).m_abstract_table_map->m_name_map


      image.png

      我们访问就是 名字(key) - value(Cache_element) 这样一个map,因为是名字比较容易看。这里我们发现元素有90个,因为比较多,值显示了小部分,实际上有90个表的字典都缓存在了Shared_dictionary_cache中。

    四、Dictionary_client的Auto_releaser类

    dd::cache::Dictionary_client::Auto_releaser实际上是一个RAII类,目的在于在析构的时候能够自动释放Dictionary_client中本次访问的字典对象。它满足先进后出的方式,它包含一个主要的元素为m_release_registry,也是一个Object_registry类型,主要目的就是将访问到的字典对象也保存在里面,以便析构自动循环它并且在dd::cache::Dictionary_client中释放

    其主要使用方式为

    • 每次定义个dd::cache::Dictionary_client::Auto_releaser类,并且将其m_client指向到会话的dd::cache::Dictionary_client
    • 当然访问字典对象的时候,同时加入到dd::cache::Dictionary_client的对应map中和dd::cache::Dictionary_client::Auto_releaser的m_release_registry对应的map中。
    • 当析构的时候自动根据dd::cache::Dictionary_client::Auto_releaser中注册的对象,在dd::cache::Dictionary_client中删除。

    下面就是这部分如下:

    size_t Dictionary_client::release(Object_registry *registry) {
      assert(registry);
      size_t num_released = 0;
      // Iterate over all elements in the registry partition.
      typename Multi_map_base<T>::Const_iterator it;
      for (it = registry->begin<T>(); it != registry->end<T>(); ++num_released) { //循环迭代Auto_relase中的响应的map对象,哑元函数确认
        // Make sure we handle iterator invalidation: Increment
        // before erasing.
        Cache_element<T> *element = it->second; //获取元素
        ++it;
        // Remove the element from the actual registry.
        registry->remove(element); //哑元函数确认,本生做删除
    
        // Remove the element from the client's object registry.
        if (registry != &m_registry_committed)
          m_registry_committed.remove(element); //client做删除
        else
          (void)m_current_releaser->remove(element);
        // Release the element from the shared cache.
        Shared_dictionary_cache::instance()->release(element);// share dict中进行计数器操作
      }
      return num_released;
    

    正是因为这样的删除方式,实际上dd::cache::Dictionary_client在语句结束后就会自动删除本身持有的字典对象,但是这个时候已经加入到了dd::cache::Shared_dictionary_cache中,因此感觉Shared_dictionary_cache的作用更大,而dd::cache::Dictionary_client和DDL联系更紧。

    五、关于字典元素

    前面说错数据一旦读取出来(如何读取后面我们会看到),就放到了字段元素这些类里面,然后挂到相应的各个map中去。
    这里以dd::Table_impl 为例,实际上有很多次的继承,很是麻烦,如下


    image.png

    每个字典元素信息都包含在这个类自身或者其父类上。

    dd::Table_impl <- Abstract_table_impl <- Entity_object_impl <- Entity_object <- Weak_object
                                                                <- Weak_object_impl <- Weak_object
                                          <- Abstract_table <- Entity_object <- Weak_object
                   <- dd::Table <- Abstract_table <- Entity_object <- Weak_object
                                            friend class cache::Storage_adapter
                                            friend class Entity_object_table_impl
    

    这里需要注意的是Entity_object 包含一个友元性质 friend class cache::Storage_adapter,这说明存储层是可以访问各个内存字典元素的数据的,获取了就可以对底层的表进行操作了,也可以操作底层的表读取数据后给内存的字典元素。

    六、字典表的属性定义

    除了字典元素本生,字典表本生也有自己的属性,比如字段/表名等等,这些属性都放到了字典表定义这个类里面,注意这也是单例,下面是和它有关的继承关系,只截取一部分。


    image.png

    比如这里Tables类,里面就要字段的定义如下


    image.png

    七、information_schema视图定义相关类

    除了上面提到的字典元素的类,字典表属性的类,建立视图还有一个类,这里简单看看。
    这部分实际上就是各个内部视图的定义,information_schema中大部分的视图都在这里定义,也就是建视图的语句都包含在这些类里面,这个太多了(注意这些类的对象也是单例)就截取一部分:

    image.png

    我们还是以information_schema.tables表为例实际上他定义的类就是dd::system_views::Tables,随便翻一下就能看到视图的定义如下:

    image.png

    下面是相关的继承关系,

    dd.system_views.Tables 
      <- dd.system_views.Tables_base 
         <- dd.system_views.System_view_impl 
            <- dd.system_views.System_view(基类)
            
    dd.system_views.System_view_select_definition_impl 
     <-dd.system_views.System_view_definition_impl
       <-dd.system_views.System_view_definition
    

    八、打开字典的流程

    实际上这里谈到的知识和原子DDL有很大关系,但是我们这里只大概看看open table的时候,在获取这种字典的时候如下做的,我们主要看如果table cache失效后,我们要拿table share如何拿的。这个函数就是get_table_share_with_discover调用的get_table_share。
    get_table_share流程大概为:

    1. 从table_def_cache中寻找是否有缓存的table share。如果有直接返回,如果没有开始做第2步。
    2. 从dd::cache::Dictionary_client中获取,这是线程自己的,如果找到直接返回,并且通过这个字典元素构建table share,并且加入table_def_cache,如果没有知道走第3步。
    3. 从dd::cache::Shared_dictionary_cache中获取,这是单例,如果找到就返回,并且加入到dd::cache::Dictionary_client中,然后构建table share(open_table_def),并且加入到table_def_cache,如果没有知道走第4步。
    4. 从底层字典表获取,找到后加入到dd::cache::Shared_dictionary_cache和dd::cache::Dictionary_client中,然后构建table share,并且加入到table_def_cache。

    在获取底层表的时候主要是通过cache::Storage_adapter::get这个方法进行的,主要是获取对应底层表的相关的一行数据,然后解析为字典元素需要的信息(Table_impl::restore_attributes)。顺便说有一下关于sdi的维护也在这个cache::Storage_adapter下进行,比如inplace DDL的最后(commit之后)会进行sdi的维护:

    mysql_alter_table 
     ->mysql_inplace_alter_table
        ->dd::cache::Dictionary_client::store
          ->dd::cache::Storage_adapter::store
            ->dd::sdi::store
    

    如果这个流程都没找到,说明你访问的表不存在。这里需要注意的是open_table_def函数,在5.7基于是frm文件构建,而到了8.0就是我们提到的这里的字典元素了。

    九、相关重点技术

    这部分新的设计完全是根据接口和实现进行的,并且多继承在那里面,回调函数使用也特别多,这里看看涉及到重点的一些C++语法的使用。

    • 哑元函数
      这个在Object_registry体现了重要的作用,因为每个Object_registry包含了好几个不同类型的字典的map,当要确认是哪个map的时候就是通过一个类Type_selector构造哑元函数进行确认的如下:


      image.png
    • const函数重载
      同上,我们可以发现他们函数的名字都是相同的,但是统一类型的map的 m_map函数有带const的又不带,不同的含义,当需要新建的时候(分配map的内存给指针)就需要不带const的,比如map的put方法,如果只是map的get方法,那么就需要带const的函数,这样就返回其指针。


      image.png
    • 友元和纯虚函数(继承)
      虽然友元自身不能继承,但是和纯虚函数一起就发生了质变。这一点在Entity_object有体现,它包含了一个友元性质 friend class cache::Storage_adapter,并且Entity_object是父类,会继承出很多的不同类型的字典类型的class子类,那么当cache::Storage_adapter访问父类Entity_object的纯虚函数的时候,如果填入的是子类的指针,实际上跑的是子类的重写的函数。

    • RAII
      这个前面已经说了,就是方便释放字典对象,或者一般的释放内存等。

    • 虚继承
      有多重继承的时候为了消除二义性。

    十、隐藏的字典表

    这部分你实际上包含好多表,需要DEBUG版本并且开启

    • SET session debug='+d,skip_dd_table_access_check';
      才能访问到,也就是我们上面谈的缓存的实际存储位置,如下:
    mysql.resource_groups
    mysql.table_stats
    mysql.routines
    mysql.events
    mysql.column_statistics
    mysql.index_stats
    mysql.tablespaces
    mysql.spatial_reference_systems
    mysql.schemata
    mysql.collations
    mysql.tables
    mysql.character_sets
    mysql.catalogs
    mysql.check_constraints
    mysql.columns
    mysql.column_type_elements
    mysql.dd_properties
    mysql.foreign_keys
    mysql.foreign_key_column_usage
    mysql.indexes
    mysql.index_column_usage
    mysql.index_partitions
    

    他们就是上面cache的实际数据的存储。其次如果我们试图查看information_schema里面表的定义我们也能够发现,其中大部分为视图,其来源就是这些内部表,比如information_schema.tables这个视图,如下:


    image.png

    而在5.7中则是memory的表如下:


    image.png

    十一、打开table share代码流程

    get_table_share_with_discover
     ->get_table_share(这个过程是一定要找到share的)
       通过table cache def寻找是否有share
       ->for 循环table_def_cache,进行寻找
       ->如果没找到,
         为shared分配内存alloc_table_share
       ->assign_new_table_id
         为share分配一个map id,share->table_map_id
       ->table_def_cache->emplace
         将key和share的智能指针放入buffer
       ->Auto_releaser releaser
         RAII自动析构,其中包含一个Object_registry元素
       ->dd::cache::Dictionary_client.acquire
         acquire(share->db.str, &sch)
         寻找schema是否存在
         ->bool dd::cache::Dictionary_client::acquire(const String_type &object_name,const T **object)
           这里的T就是进行实例化比如这里的sch
           ->bool Dictionary_client::acquire(const K &key, const T **object,bool *local_committed,bool *local_uncommitted)
            ->m_registry_committed.get(key, &element); 
              现在本地的Dictionary_client map 中寻找,如果找到直接返回
            ->如果没有找到在全局shared中查找
              Shared_dictionary_cache::instance()->get(m_thd, key, &element)
              具体流程见后面
            ->m_registry_committed.put(element);
              插入到本地Dictionary_client map中
       ->open_table_def  
         根据找到的字典元素进行share构建
        
       
       ->Shared_dictionary_cache::instance()->get(m_thd, key, &element)
         当client没有命中则调用
         ->Shared_dictionary_cache::get(THD *thd, const K &key,Cache_element<T> **element)
           用于获取字段对象
           ->m_map<T>()->get(key, element)
             从对应的map中获取,如果获取失败则调用引擎层进行查询
             ->Shared_dictionary_cache::get_uncached(thd, key, ISO_READ_COMMITTED, &new_object)
               这里需要的是一个新的字典对象,传入指针的指针
               ->Storage_adapter::get(thd, key, isolation, false, object)
                 dd::cache::Storage_adapter::get
                 调用接口查询数据
                 ->Entity_object_table &table = T::DD_table::instance(); 
                   通过字典对象的属性(typedef tables::Tables DD_table;)获取字典表的信息
                   比如tables
                 ->Raw_table *t = trx.otx.get_table(table.name()); 
                   获取字典表访问的接口
                 ->t->find_record(key, r)
                   查询数据
                 ->table.restore_object_from_record(&trx.otx, *r.get(), &new_object)
                   将获取的底层字典表的信息,存储到new_object这个字典对象中,比如这是一个
                   表的信息。实际上调用为对应字典对象类的
                   ->Table_impl::restore_attributes(const Raw_record &r)                    
           ->m_map<T>()->put(&key, new_object, element);
             插入share cache对应的map中
    

    以上。。

    参考:
    MySQL · 源码分析 · 原子DDL的实现过程
    MySQL 深潜 - 一文详解 MySQL Data Dictionary
    MySQL8.0数据字典实现一窥

    相关文章

      网友评论

        本文标题:MySQL:8.0全新的字典缓存(代替5.7 frm文件)

        本文链接:https://www.haomeiwen.com/subject/prdfartx.html