背景介绍

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 client线程独享,核心数据结构为Local_multi_map,用于加速当前线程对于同一对象的重复访问,以及在当前线程执行DDL语句修改DD对象时管理已提交、未提交、删除状态的对象。
  • 第二级缓存是共享缓存,为所有线程共享的全局缓存,核心数据结构为Shared_multi_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组成。

  1. class Dictionary_client {
  2. private:
  3. std::vector<Entity_object *> m_uncached_objects; // Objects to be deleted.
  4. Object_registry m_registry_committed; // Registry of committed objects.
  5. Object_registry m_registry_uncommitted; // Registry of uncommitted objects.
  6. Object_registry m_registry_dropped; // Registry of dropped objects.
  7. THD *m_thd; // Thread context, needed for cache misses.
  8. ...
  9. };

代码段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来实现。

  1. Shared_dictionary_cache *Shared_dictionary_cache::instance() {
  2. static Shared_dictionary_cache s_cache;
  3. return &s_cache;
  4. }

代码段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加入到一级缓存时,已经确保其在二级缓存中也备份了一份,以供其他线程使用。

代码实现如下:

  1. // Get a dictionary object.
  2. template <typename K, typename T>
  3. bool Dictionary_client::acquire(const K &key, const T **object,
  4. bool *local_committed,
  5. bool *local_uncommitted) {
  6. ...
  7.  
  8. // Lookup in registry of uncommitted objects
  9. T *uncommitted_object = nullptr;
  10. bool dropped = false;
  11. acquire_uncommitted(key, &uncommitted_object, &dropped);
  12.  
  13. ...
  14.  
  15. // Lookup in the registry of committed objects.
  16. Cache_element<T> *element = NULL;
  17. m_registry_committed.get(key, &element);
  18.  
  19. ...
  20.  
  21. // Get the object from the shared cache.
  22. if (Shared_dictionary_cache::instance()->get(m_thd, key, &element)) {
  23. DBUG_ASSERT(m_thd->is_system_thread() || m_thd->killed ||
  24. m_thd->is_error());
  25. return true;
  26. }
  27.  
  28. ...
  29. }

代码段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.不需要每个线程都去持久化存储中读取数据,避免资源的浪费。

并发控制的代码如下:

  1. // Get a wrapper element from the map handling the given key type.
  2. template <typename T>
  3. template <typename K>
  4. bool Shared_multi_map<T>::get(const K &key, Cache_element<T> **element) {
  5. Autolocker lock(this);
  6. *element = use_if_present(key);
  7. if (*element) return false;
  8.  
  9. // Is the element already missed?
  10. if (m_map<K>()->is_missed(key)) {
  11. while (m_map<K>()->is_missed(key))
  12. mysql_cond_wait(&m_miss_handled, &m_lock);
  13.  
  14. *element = use_if_present(key);
  15.  
  16. // Here, we return only if element is non-null. An absent element
  17. // does not mean that the object does not exist, it might have been
  18. // evicted after the thread handling the first cache miss added
  19. // it to the cache, before this waiting thread was alerted. Thus,
  20. // we need to handle this situation as a cache miss if the element
  21. // is absent.
  22. if (*element) return false;
  23. }
  24.  
  25. // Mark the key as being missed.
  26. m_map<K>()->set_missed(key);
  27. return true;
  28. }

代码段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更新到缓存中去,先更新二级缓存,再更新一级缓存,流程图如下:

图5 数据字典缓存更新流程图

因为这个更新DD缓存的操作是在事务commit阶段进行,所以在更新一级缓存时,会先把更新后的DD cache object放到一级缓存中的m_registry_committed里去,再把m_registry_uncommitted和m_registry_dropped清空。

缓存失效过程

当Dictionary_client的drop方法被调用对元数据对象进行清理时,在元数据对象从DD tables中删除后,会调用invalidate()函数使两级缓存中的DD cache object失效。流程图如下:

图6 数据字典缓存失效流程图

这里在判断DD cache object在一级缓存中存在,并在一级缓存中删除掉该对象后,可以直接在二级缓存中完成删除操作。缓存失效的过程受到元数据锁(Metadata lock, MDL)的保护,因为元数据锁的并发控制,保证了一个线程在删除共享缓存时,不会有其他线程也来删除它。实际上本地缓存的数据有效,就是依赖于元数据锁的保护,否则共享缓存区域的信息,是可以被其他线程更改的。

缓存容量管理

一级本地缓存为DD client线程独享,由RAII类Auto_releaser来负责管理其生命周期。其具体流程为:每次建立一个DD client时,会定义一个对应的Auto_releaser类,当访问DD时,会把读取到的DD cache object同时加到Auto_releaser里面的m_release_registry中去,当Auto_releaser析构时,会调用Dictionary_client的release()函数把m_release_registry中的DD缓存全部释放掉。

二级共享缓存会在Shared_dictionary_cache初始化时,根据不同类型的对象设定好缓存的容量,代码如下:

  1. void Shared_dictionary_cache::init() {
  2. instance()->m_map<Collation>()->set_capacity(collation_capacity);
  3. instance()->m_map<Charset>()->set_capacity(charset_capacity);
  4. ...
  5. }

代码段5

在二级缓存容量达到上限时,会通过LRU的缓存淘汰策略来淘汰最近最少使用的DD cache对象。在一级缓存中存在的缓存对象不会被淘汰。

  1. // Helper function to evict unused elements from the free list.
  2. template <typename T>
  3. void Shared_multi_map<T>::rectify_free_list(Autolocker *lock) {
  4. mysql_mutex_assert_owner(&m_lock);
  5. while (map_capacity_exceeded() && m_free_list.length() > 0) {
  6. Cache_element<T> *e = m_free_list.get_lru();
  7. DBUG_ASSERT(e && e->object());
  8. m_free_list.remove(e);
  9. // Mark the object as being used to allow it to be removed.
  10. e->use();
  11. remove(e, lock);
  12. }
  13. }

代码段6

总结

MySQL 8.0中的数据字典,通过对两级缓存的逐级访问,以及精妙的对缓存未命中情况的处理方式,有效的加速了在不同场景下数据库对DD的访问速度,显著的提升了数据库访问元数据信息的效率。另外本文还提到了元数据锁对数据字典缓存的保护,关于元数据锁的相关机制,会在后续文章陆续介绍。

点击关注,第一时间了解华为云新鲜技术~

解读MySQL 8.0数据字典缓存管理机制的更多相关文章

  1. Redis之Redis缓存管理机制

    Redis缓存管理机制 目录 Redis缓存管理机制 缓存过期 && 缓存淘汰 缓存穿透 && 布隆过滤器 缓存击穿 && 缓存雪崩 总结 彩蛋 从博客 ...

  2. MySQL 8.0 —— 数据字典

    1.简介 MySQL 8.0 将数据库元信息都存放于InnoDB存储引擎表中,在之前版本的MySQL中,数据字典不仅仅存放于特定的存储引擎表中,还存放于元数据文件.非事务性存储引擎表中.本文将会介绍M ...

  3. 干货 | 解读MySQL 8.0新特性:Skip Scan Range

    MySQL从8.0.13版本开始支持一种新的range scan方式,称为Loose Skip Scan.该特性由Facebook贡献.我们知道在之前的版本中,如果要使用到索引进行扫描,条件必须满足索 ...

  4. MySQL 8.0 Undo Tablespace管理

    目录 1. UNDO 基础概念 2. UNDO 相关参数 2.1 参数含义 3. UNDO 表空间运维 3.1 查看UNDO的基本信息 3.2 添加/active/inactive/删除UNDO表空间 ...

  5. WEB缓存控制机制与varnish简介

    在说到缓存varnish前,我们首先来了解下对于web服务缓存到底是什么?它有哪些特点,基础原理是什么? http是web应用协议,通常我们说的一次http事务,不外乎就是客户端请求,服务端响应,通常 ...

  6. 【MySQL】InnoDB 内存管理机制 --- Buffer Pool

    InnoDB Buffer Pool 是一块连续的内存,用来存储访问过的数据页面 innodb_buffer_pool_size 参数用来定义 innodb 的 buffer pool 的大小 是 M ...

  7. 分布式缓存系统 Memcached 内存管理机制

    在前面slab数据存储部分分析了Memecached中记录数据的具体存储机制,从中可以看到所采用的内存管理机制——slab内存管理,这也正是linux所采用的内存高效管理机制,对于Memchached ...

  8. mysql开启缓存、设置缓存大小、缓存过期机制

    目录 一.开启缓存 1.修改配置文件my.ini 2.命令方式 二.查看是否生效 1.query_cache_type 使用查询缓存的方式 2.have_query_cache 设置查询缓存是否可用 ...

  9. SpringBoot缓存管理(三) 自定义Redis缓存序列化机制

    前言 在上一篇文章中,我们完成了SpringBoot整合Redis进行数据缓存管理的工作,但缓存管理的实体类数据使用的是JDK序列化方式(如下图所示),不便于使用可视化管理工具进行查看和管理. 接下来 ...

  10. MySQL内存管理机制浅析

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. GreatSQL是MySQL的国产分支版本,使用上与MySQL一致. 目录 一.placement new的定义 二.pl ...

随机推荐

  1. 4G EPS 的架构模型

    目录 文章目录 目录 前文列表 EPS 的架构 EPS 的架构模型 E-UTRAN UE eNodeB EPC MME(移动性管理) SGW(本地移动性锚点) PGW(业务锚点) HSS(用户认证及鉴 ...

  2. Vue+springboot集成PageOffice实现在线编辑Word、excel文档

    说明: PageOffice是一款在线的office编辑软件,帮助Web应用系统或Web网站实现用户在线编辑Word.Excel.PowerPoint文档.可以完美实现在线公文流转,领导批阅,盖章.可 ...

  3. CodePen 的国内替代「笔.COOL」,一个功能完备、使用便捷的在线HTML/CSS/JS编辑器和作品分享平台

    笔.COOL,是一个最近在国内崭露头角的在线HTML/CSS/JS编辑器和作品分享平台. 笔.COOL 提供了一个在线的 HTML.CSS 和 JavaScript 代码编辑器.无需任何安装,你只需打 ...

  4. QGIS开发笔记(三):Windows安装版二次开发环境搭建(下):将QGis融入QtDemo,添加QGis并加载tif遥感图的Demo

    前言   使用QGis的目的是进行二次开发,或者说是融入我们的应用(无人车.无人船.无人机),本片描述搭建QGis二次基础开发环境,由于实在是太长了,进行了分篇: 上半部分:主要是安装好后,使用QtC ...

  5. 利用 Helm 在各类 Kubernetes 中安装 Rainbond

    利用 Helm 安装 Rainbond 好雨科技技术团队一直致力于让用户更方便的安装 Rainbond 这款产品. 不久前,我们刚刚推出了在一个容器中部署 Rainbond 的快速安装方式,这种方式覆 ...

  6. Leetcode-937-Reorder Log Files-(Easy)

    一.题目描述 You have an array of logs.  Each log is a space delimited string of words. For each log, the ...

  7. 使用sysdig查看容器里的系统调用

    目录 一.系统环境 二.前言 三.系统调用简介 四.Sysdig简介 五.使用sysdig查看容器里的系统调用 5.1 以二进制包的形式安装sysdig 5.2 使用sysdig查看容器里的系统调用 ...

  8. openstack nova 报错

    openstack compute service list The server is currently unavailable. Please try again at a later time ...

  9. (二)requests-爬取国家药监局生产许可证数据

    首先访问这个页面 url = 'http://125.35.6.84:81/xk/' 我们的目标是抓取这里的每一个企业的详情页数据,但是可以发现这里只有企业的简介信息,所以这就意味着我们要发送两次ge ...

  10. javascript class 方法的this指向问题

    踩坑记录 JavaScript 的 class 里面有两种定义方法的方式 普通函数(fun1) 箭头函数(fun2) class Obj { func1() { // write some code. ...