解读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数据字典缓存管理机制的更多相关文章
- Redis之Redis缓存管理机制
Redis缓存管理机制 目录 Redis缓存管理机制 缓存过期 && 缓存淘汰 缓存穿透 && 布隆过滤器 缓存击穿 && 缓存雪崩 总结 彩蛋 从博客 ...
- MySQL 8.0 —— 数据字典
1.简介 MySQL 8.0 将数据库元信息都存放于InnoDB存储引擎表中,在之前版本的MySQL中,数据字典不仅仅存放于特定的存储引擎表中,还存放于元数据文件.非事务性存储引擎表中.本文将会介绍M ...
- 干货 | 解读MySQL 8.0新特性:Skip Scan Range
MySQL从8.0.13版本开始支持一种新的range scan方式,称为Loose Skip Scan.该特性由Facebook贡献.我们知道在之前的版本中,如果要使用到索引进行扫描,条件必须满足索 ...
- MySQL 8.0 Undo Tablespace管理
目录 1. UNDO 基础概念 2. UNDO 相关参数 2.1 参数含义 3. UNDO 表空间运维 3.1 查看UNDO的基本信息 3.2 添加/active/inactive/删除UNDO表空间 ...
- WEB缓存控制机制与varnish简介
在说到缓存varnish前,我们首先来了解下对于web服务缓存到底是什么?它有哪些特点,基础原理是什么? http是web应用协议,通常我们说的一次http事务,不外乎就是客户端请求,服务端响应,通常 ...
- 【MySQL】InnoDB 内存管理机制 --- Buffer Pool
InnoDB Buffer Pool 是一块连续的内存,用来存储访问过的数据页面 innodb_buffer_pool_size 参数用来定义 innodb 的 buffer pool 的大小 是 M ...
- 分布式缓存系统 Memcached 内存管理机制
在前面slab数据存储部分分析了Memecached中记录数据的具体存储机制,从中可以看到所采用的内存管理机制——slab内存管理,这也正是linux所采用的内存高效管理机制,对于Memchached ...
- mysql开启缓存、设置缓存大小、缓存过期机制
目录 一.开启缓存 1.修改配置文件my.ini 2.命令方式 二.查看是否生效 1.query_cache_type 使用查询缓存的方式 2.have_query_cache 设置查询缓存是否可用 ...
- SpringBoot缓存管理(三) 自定义Redis缓存序列化机制
前言 在上一篇文章中,我们完成了SpringBoot整合Redis进行数据缓存管理的工作,但缓存管理的实体类数据使用的是JDK序列化方式(如下图所示),不便于使用可视化管理工具进行查看和管理. 接下来 ...
- MySQL内存管理机制浅析
GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. GreatSQL是MySQL的国产分支版本,使用上与MySQL一致. 目录 一.placement new的定义 二.pl ...
随机推荐
- PPO近端策略优化玩cartpole游戏
这个难度有些大,有两个policy,一个负责更新策略,另一个负责提供数据,实际这两个policy是一个东西,用policy1跑出一组数据给新的policy2训练,然后policy2跑数据给新的poli ...
- 智能运维|AIRIOT智慧光伏管理解决方案
随着新能源发展到今天,我国的能源产业已经形成产业化规模化的发展,"光伏能源"被广泛应用于电力.农业.市政照明甚至是军事领域. 以光伏电站为例,大量铺设的太阳能板运维成本相当高, ...
- vulnhub靶场 --> Red: 1
靶机下载地址 Red: 1 << 点我 开始打靶 IP发现 nmap扫描网段发现靶机ip:192.168.111.142 端口发现 对靶机进行常规端口扫描 访问网站 到处点击发现存在一个可 ...
- 【u8 login debug】u8 16.0 没有调试 login的解决办法
16.0 没有调试 login,改一下注册表 就行[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Ufsoft\WF\V8.700]"Enable.Debu ...
- FFmpeg下载编译、代码结构以及编译系统
从这里开始,就要踏上学习FFmpeg的旅程了,使用的FFmpeg版本5.0.1 1.ubuntu下,如何下载并编译FFmpeg源码 打开FFmpeg官网 Download FFmpeg,我们可以通过g ...
- #define、const和enum
enum:枚举类型(枚举变量的值只能等于枚举中定义的常量) #define:明示常量(定义真正的常量) const:限定符(名不符实,应该叫read only),限定一个变量为只读 C语言常量: 1. ...
- pandas基础--缺失数据处理
pandas含有是数据分析工作变得更快更简单的高级数据结构和操作工具,是基于numpy构建的. 本章节的代码引入pandas约定为:import pandas as pd,另外import numpy ...
- Java中Calendar类与SimpleDateFormat类的介绍
目录 Calendar类(关于日期的一些方法) get(Calendar.XXX); get(Calendar.Year) get(Calendar.MONTH) get(Calendar.DAY_O ...
- c# winfrom DataGridView 动态UI下载功能(内含GIF图) || 循环可变化的集合 数组 datatable 等
Gif演示 分解步骤 1,使用组件DataGridView 2,使用DataSource来控制表格展示的数据来源(注意:来源需要是DataTable类型) 3,需要用到异步线程.如果是不控制数据源的话 ...
- Dva.js 快速上手指南
先说些废话 最近在开发React技术栈的项目产品,对于数据状态的管理使用了Dva.js,作为一个资深的ow玩家,我看到这个名字第一反应就是----这不是ow里的一个女英雄吗?仔细阅读了官方文档之后,发 ...