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


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


一、综述

在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> m_abstract_table_map;
  std::unique_ptr> m_charset_map;
  std::unique_ptr> m_collation_map;
  std::unique_ptr> m_column_statistics_map;
  std::unique_ptr> m_event_map;
  std::unique_ptr> m_resource_group_map;
  std::unique_ptr> m_routine_map;
  std::unique_ptr> m_schema_map;
  std::unique_ptr>

每种类型的元数据都缓存在自己的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> m_rev_map;  // Reverse element map.
  Element_map>
      m_id_map;  // Id map instance.
  Element_map>
      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 m_id_key;      // The id key for the object.
  Key_wrapper m_name_key;  // The name key for the object.
  Key_wrapper 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
  • m_registry_committed
    加入时机: dd::cache::Dictionary_client::remove_uncommitted_objects
    比如实例化的函数:
    dd::cache::Dictionary_client::remove_uncommitted_objects
  • m_registry_dropped
    加入时机: dd::cache::Dictionary_client::register_dropped_object
    比如实例化的函数: dd::cache::Dictionary_client::register_dropped_object

实际上这部分主要和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()->set_capacity(collation_capacity);
  instance()->m_map()->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()->set_capacity(max_connections);
  instance()->m_map()->set_capacity(event_capacity);
  instance()->m_map()->set_capacity(stored_program_def_size);
  instance()->m_map()->set_capacity(schema_def_size);
  instance()->m_map()->set_capacity(
      column_statistics_capacity);
  instance()->m_map()->set_capacity(
      spatial_reference_system_capacity);
  instance()->m_map()->set_capacity(tablespace_def_size);
  instance()->m_map()->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
size_t Dictionary_client::release(Object_registry *registry)
如下:

    // Release the element from the shared cache.
    Shared_dictionary_cache::instance()->release(element);//在share中去除

这里就是调用的
  template 
  void release(Cache_element *e) {
    m_map()->release(e);
  }

实际上就是,Shared_multi_map的release方法,关键如下:

Shared_multi_map::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::Const_iterator it;
  for (it = registry->begin(); it != registry->end(); ++num_released) { //循环迭代Auto_relase中的响应的map对象,哑元函数确认
    // Make sure we handle iterator invalidation: Increment
    // before erasing.
    Cache_element *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 **element)
       用于获取字段对象
       ->m_map()->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()->put(&key, new_object, element);
         插入share cache对应的map中

以上。。

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

你可能感兴趣的:(MySQL:8.0全新的字典缓存(代替5.7 frm文件))