MySQL的数据字典(Data Dictionary,简称DD),用于存储数据库的元数据信息,它在8.0版本中被重新设计和实现,通过将所有DD数据唯一地持久化到InnoDB存储引擎的DD tables,实现了DD的统一管理。为了避免每次访问DD都去存储中读取数据,使DD内存对象能够复用,DD实现了两级缓存的架构,这样在每个线程使用DD client访问DD时可以通过两级缓存来加速对DD的内存访问.
图1 数据字典缓存架构图 。
需要访问DD的数据库工作线程通过建立一个DD client(DD系统提供的一套DD访问框架)来访问DD,具体流程为通过与线程THD绑定的类Dictionary_client,来依次访问一级缓存和二级缓存,如果两级缓存中都没有要访问的DD对象,则会直接去存储在InnoDB的DD tables中去读取。后文会详细介绍这个过程.
DD的两级缓存底层都是基于std::map,即键值对来实现的.
整个DD cache的相关类图结构如下:
图2 数据字典缓存类图 。
Element_map是对std::map的一个封装,键是id、name等,值是Cache_element,它包含了DD cache object,以及对该对象的引用计数。DD cache object就是我们要获取的DD信息.
Multi_map_base中包含了多个Element_map,可以让用户根据不同类型的key来获取缓存对象。Local_multi_map和Shared_multi_map都是继承于Multi_map_base.
第一级缓存,即本地缓存,位于每个Dictionary_client内部,由不同状态(committed、uncommitted、dropped)的Object_registry组成.
class Dictionary_client { private: std::vector<Entity_object *> m_uncached_objects; // Objects to be deleted. Object_registry m_registry_committed; // Registry of committed objects. Object_registry m_registry_uncommitted; // Registry of uncommitted objects. Object_registry m_registry_dropped; // Registry of dropped objects. THD *m_thd; // Thread context, needed for cache misses. ... };
代码段1 。
其中m_registry_committed,存放的是DD client访问DD时已经提交且可见的DD cache object。如果DD client所在的当前线程执行的是一条DDL语句,则会在执行过程中将要drop的旧表对应的DD cache object存放在m_registry_dropped中,将还未提交的新表定义对应的DD cache object存放在m_registry_uncommitted中。在事务commit/rollback后,会把m_registry_uncommitted中的DD cache object更新到m_registry_committed中去,并把m_registry_uncommitted和m_registry_dropped清空.
每个Object_registry由不同元数据类型的Local_multi_map组成,通过模板的方式,实现对不同类型的对象(比如表、schema、tablespace、Event 等)缓存的管理.
第二级缓存,即共享缓存,是全局唯一的,使用单例Shared_dictionary_cache来实现.
Shared_dictionary_cache *Shared_dictionary_cache::instance() { static Shared_dictionary_cache s_cache; return &s_cache; }
代码段2 。
与本地缓存中Object_registry相似,Shared_dictionary_cache也包含针对各种类型对象的缓存。与本地缓存的区别在于,本地缓存可以无锁访问,而共享缓存需要在获取/释放DD cache object时进行加锁来完成并发控制,并会通过Shared_multi_map中的条件变量来完成并发访问中的线程同步与缓存未命中情况的处理.
逻辑流程 。
DD对象主要有两种访问方式,即通过元数据的id,或者name来访问。需要访问DD的数据库工作线程通过DD client,传入元数据的id,name等key去缓存中读取元数据对象。读取的整体过程:一级本地缓存 -> 二级共享缓存 -> 存储引擎。流程图如下:
图3 数据字典缓存读取流程图 。
由上图所示,在DD cache object加入到一级缓存时,已经确保其在二级缓存中也备份了一份,以供其他线程使用.
代码实现如下:
// 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; } ... }
代码段3 。
在一级本地缓存中读取时,会先去m_registry_uncommitted和m_registry_dropped中读取(均在acquire_uncommitted()函数中实现),因为这两个是最新的修改。之后再去m_registry_committed中读取,如果读取到就直接返回,否则去二级共享缓存中尝试读取。共享缓存的读取过程在Shared_multi_map::get()中实现。就是加锁后直接到对应的Element_map中查找,存在则把其加入到一级缓存中并返回;不存在,则会进入到缓存未命中的处理流程.
缓存未命中 。
当本地缓存和共享缓存中都没有读取到元数据对象时,就会调用DD cache的持久化存储的接口Storage_adapter::get()直接从存储在InnoDB中的DD tables中读取,创建出DD cache object后,依次把其加入到共享缓存和本地缓存中.
DD client对并发访问未命中缓存的情况做了并发控制,这样做有以下几个考量:
1.因为内存对象可以共用,所以只需要维护一个DD cache object在内存即可.
2.访问持久化存储的调用栈较深,可能涉及IO,比较耗时.
3.不需要每个线程都去持久化存储中读取数据,避免资源的浪费.
并发控制的代码如下:
// Get a wrapper element from the map handling the given key type. template <typename T> template <typename K> bool Shared_multi_map<T>::get(const K &key, Cache_element<T> **element) { Autolocker lock(this); *element = use_if_present(key); if (*element) return false; // Is the element already missed? if (m_map<K>()->is_missed(key)) { while (m_map<K>()->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<K>()->set_missed(key); return true; }
代码段4 。
第一个访问未命中缓存的DD client会将key加入到Shared_multi_map的m_missed集合中,这个集合包含着现在所有正在读取DD table中元数据的对象key值。之后的client在访问DD table之前会先判断目标key值是否在m_missed集合中,如在,就会进入等待。当第一个DD client构建好DD cache object,并把其加入到共享缓存之后,移除m_missed集合中对应的key,并通过条件变量通知所有等待的线程重新在共享缓存中获取。这样对于同一个DD cache object,就只会对DD table访问一次了。时序图如下:
图4 数据字典缓存未命中时序图 。
在一个数据库工作线程对DD进行修改时,DD cache也会在事务commit阶段通过remove_uncommitted_objects()函数进行更新,更新的过程为先把DD旧数据从缓存中删除,再把修改后的DD cache object更新到缓存中去,先更新二级缓存,再更新一级缓存,流程图如下: