查看原文
其他

再见了,frm,MySQL 8.0全新字典缓存

老叶茶馆 2024-07-08

The following article is from MySQL学习 Author 高鹏(八怪)

先推荐高鹏、徐轶韬两位老师的书

一、综述
从MySQL 8.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,这里需要注意的是他们都是父类,比如

当然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::updatedd::Table
  • m_registry_committed 加入时机: dd::cache::Dictionary_client::remove_uncommitted_objects 比如实例化的函数: dd::cache::Dictionary_client::remove_uncommitted_objectsdd::Table
  • m_registry_dropped 加入时机: dd::cache::Dictionary_client::register_dropped_object 比如实例化的函数: dd::cache::Dictionary_client::register_dropped_objectdd::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变量,如下:

实际上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 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我们访问就是 名字(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 为例,实际上有很多次的继承,很是麻烦,如下

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

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,这说明存储层是可以访问各个内存字典元素的数据的,获取了就可以对底层的表进行操作了,也可以操作底层的表读取数据后给内存的字典元素。

六、字典表的属性定义

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

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

七、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构造哑元函数进行确认的如下:

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

  • 友元和纯虚函数(继承), 虽然友元自身不能继承,但是和纯虚函数一起就发生了质变。这一点在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这个视图,如下:

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

十一、打开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的实现过程 https://developer.aliyun.com/article/560507

  • MySQL 深潜 - 一文详解 MySQL Data Dictionary https://developer.aliyun.com/article/788173

  • MySQL8.0数据字典实现一窥 https://cloud.tencent.com/developer/article/1496677

以上。。



《深入浅出MGR》视频课程

戳此小程序即可直达B站

https://www.bilibili.com/medialist/play/1363850082?business=space_collection&business_id=343928&desc=0



文章推荐:



想看更多技术好文,点个“在看”吧!

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存