解读MySQL 8.0数据字典缓存管理机制

mysql · 浏览次数 : 62

小编点评

MySQL 8.0中的数据字典(Data Dictionary,简称DD)是一个重要的组件,用于存储和管理数据库的元数据信息。在8.0版本中,MySQL对数据字典进行了重新设计和实现,以提高数据库访问元数据信息的效率。 **数据字典缓存架构** MySQL的数据字典缓存采用了两级缓存架构,包括本地缓存和共享缓存。本地缓存由每个DD client线程独享,而共享缓存为所有线程共享。这种设计可以减少内存占用,并提高缓存访问速度。 **缓存读取过程** 当数据库工作线程需要访问DD时,它会通过DD client与存储引擎交互。首先,它会检查本地缓存(Local_multi_map)和共享缓存(Shared_multi_map)中是否有需要的DD对象。如果没有,它会直接访问存储在InnoDB中的DD tables。 **缓存未命中处理** 如果缓存未命中,MySQL会调用持久化存储的接口(Storage_adapter::get())从存储引擎中读取DD对象。读取完成后,会将对象加入共享缓存和本地缓存中,以便其他线程可以使用。 **缓存修改过程** 当数据库工作线程对DD进行修改时,DD cache会在事务commit阶段通过remove_uncommitted_objects()函数进行更新。更新完成后,会通知其他线程刷新缓存。 **缓存失效过程** 当DD client的drop方法被调用时,会从DD tables中删除元数据对象。为了保护数据的一致性,这个过程会受到元数据锁(Metadata lock, MDL)的保护。 **缓存容量管理** MySQL通过RAII类Auto_releaser来管理本地缓存的生命周期。当访问DD时,会将读取到的DD对象加入Auto_releaser中的m_release_registry。当Auto_releaser析构时,会调用Dictionary_client的release()函数释放所有DD缓存。 **总结** MySQL 8.0中的数据字典缓存通过两级缓存的逐级访问和精妙的缓存未命中处理方式,有效地提高了数据库访问元数据信息的效率。此外,元数据锁的保护也确保了数据的一致性。

正文

背景介绍

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组成。

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

图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初始化时,根据不同类型的对象设定好缓存的容量,代码如下:

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

代码段5

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

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

代码段6

总结

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

 

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

 

与解读MySQL 8.0数据字典缓存管理机制相似的内容:

解读MySQL 8.0数据字典缓存管理机制

MySQL 8.0中的数据字典,通过对两级缓存的逐级访问,以及精妙的对缓存未命中情况的处理方式,有效的加速了在不同场景下数据库对DD的访问速度,显著的提升了数据库访问元数据信息的效率。

解读MySQL 8.0数据字典的初始化与启动

本文分享自华为云社区《MySQL全文索引源码剖析之Insert语句执行过程》,作者:GaussDB 数据库。 本文主要介绍MySQL 8.0数据字典的基本概念和数据字典的初始化与启动加载的主要流程。 MySQL 8.0数据字典简介 数据字典(Data Dictionary, DD)用来存储数据库内部

[转帖]MySQL 8.0 Instant Add Column功能解析

https://zhuanlan.zhihu.com/p/408702204 概述 DDL(Data Definition Language)是数据库内部的对象进行创建、删除、修改的操作语言,主要包括:加减列、更改列类型、加减索引等类型。数据库的模式(schema)会随着业务的发展不断变化,如果没有

MySQL 执行计划详解

本文从EXPLAIN分析SQL的执行计划开始,进行示例展示,并对输出结果进行解读,同时总结了EXPLAIN可产生额外的扩展信息以及EXPLAIN的估计查询性能,整篇文章基于MySQL 8.0编写,理论支持MySQL 5.0及更高版本。

mysql-8.4.0解压版安装记录

MySQL 8.4.0解压版安装记录 这几天,安装最新版mysql 8.4的时候,遇到了不少问题,网上的教程大多数都是旧版本的,也安装不成功。 参考了大量教程后,经过自己的摸索终于装好了,这里记录一下。 我下载的是8.4.0 LTS MySQL :: Download MySQL Community

[转帖]Oldguo-MySQL 5.6 ,5.7 ,8.0在安装部署的异同

Oldguo-MySQL 5.6 ,5.7 ,8.0在安装部署的异同 https://www.jianshu.com/p/6f2cb7874abd 5.6.44 二进制包安装部署 解压到以下目录 [root@oldboy ~]# ll /usr/local/mysql56/ drwxr-xr-x.

解读GaussDB(for MySQL)灵活多维的二级分区表策略

本文分享自华为云社区《GaussDB(for MySQL)创新特性:灵活多维的二级分区表策略》,作者:GaussDB 数据库。 背景介绍 分区表及二级分区表的功能,可以让数据库更加有效地管理和查询大规模数据,传统商业数据库具备该能力。MySQL支持分区表,与传统商业数据库相比,MySQL对二级分区表

SpringBoot 集成 Quartz + MySQL

Quartz 简单使用 Java SpringBoot 中,动态执行 bean 对象中的方法 源代码地址 => https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-quartz 工作原理解读 只要配置好 DataSource Quartz 会

京东云TiDB SQL优化的最佳实践

京东云TiDB SQL层的背景介绍 从总体上概括 TiDB 和 MySQL 兼容策略,如下表: SQL层的架构 用户的 SQL 请求会直接或者通过 Load Balancer 发送到 京东云TiDB Server,TiDB Server 会解析 MySQL Protocol Packet,获取请求内

Python史上最全种类数据库操作方法,你能想到的数据库类型都在里面!甚至还有云数据库!

本文将详细探讨如何在Python中连接全种类数据库以及实现相应的CRUD(创建,读取,更新,删除)操作。我们将逐一解析连接MySQL,SQL Server,Oracle,PostgreSQL,MongoDB,SQLite,DB2,Redis,Cassandra,Microsoft Access,El