码农知识堂 - 1000bd
  •   Python
  •   PHP
  •   JS/TS
  •   JAVA
  •   C/C++
  •   C#
  •   GO
  •   Kotlin
  •   Swift
  • 解读MySQL 8.0数据字典缓存管理机制


    MySQL 8.0中的数据字典,通过对两级缓存的逐级访问,以及精妙的对缓存未命中情况的处理方式,有效的加速了在不同场景下数据库对DD的访问速度,显著的提升了数据库访问元数据信息的效率。
    合集 - 数据库最新分享(71)
    1.华为云峰会2024,GaussDB扬帆出海,给世界一个更优选择02-282.预算有限,资源冗余?DWS集群缩容如何帮你解决烦劳02-293.数智融合,华为云GaussDB(for MySQL)助力企业释放数据新价值03-014.RDS for MySQL Serverless公测上线:弹性伸缩,最高可降成本超80%03-045.GaussDB跨云容灾:实现跨地域的数据库高可用能力03-056.实例详解如何构建动态SQL语句03-057.GaussDB(DWS)运维利刃:TopSQL工具解析03-078.手把手带你认识GaussDB轻量化运维管理工具03-089.守护更多女性健康,华为云GeminiDB助力美柚数据库高效稳定迁移03-0810.GaussDB(DWS)集群通信:详解pooler连接池03-1111.Libcomm通信库:GaussDB(DWS) 为解决建联过多的小妙招03-1212.实例带你了解GaussDB数据库的LOCK TABLE03-1213.RDS for MariaDB“智能DBA助手”,让运维效率嗖嗖地!03-1214.实例带你了解GaussDB的索引管理03-1415.详解GaussDB(DWS)中3个防过载检查项03-1816.华为云数据库创新发展论坛,打造行业更优数据库底座!03-1917.GaussDB(分布式)实例故障处理03-1918.华为云GeminiDB新版本发布:全面支持Redis 6.203-2019.究竟什么样的数据库,才能承接RTA广告这个技术活!03-2020.GaussDB(DWS) 业务高可靠的三大利器:CN RETRY、远程读、ELB03-2521.走在前、做示范,苏州农商银行携华为云完成超级网银系统改造03-2522.分布式数据库技术的演进和发展方向03-2623.新版Redis不再“开源”,对使用者都有哪些影响?03-2724.cgroup、资源池、用户的关系..涉及到GaussDB(DWS)的资源设置03-2925.GeminiDB Cassandra接口新特性FLASHBACK发布:任意时间点秒级闪回04-0126.探索GaussDB(DWS)湖仓融合:Hudi与元数据打通的深度解析04-0127.详解数仓对象设计中序列SEQUENCE原理与应用04-0228.数仓调优实战:GUC参数调优04-0729.详讲openGauss 5.0 单点企业版如何部署_Centos7_x8604-0830.华为云GeminiDB,广告RTA的“登云梯”04-0931.DTC2024,华为云数据库创新融合大发展,打造世界级数据库!04-1732.GaussDB(DWS)基于Flink的实时数仓构建04-1833.数仓的两种轻量级数据交换格式:json与jsonb04-1934.重磅新品发布!云耀数据库HRDS,享受轻量级的极致体验04-2335.“企业创新新引擎”数据库专项赋能会,让云原生技术普惠千行百业!04-2436.GaussDB SQL查询语句执行过程解析04-2437.详解数仓的向量化执行引擎04-2538.Redis开源社区持续壮大,华为云为Valkey项目注入新的活力05-0639.详解数仓的3A安全能力05-0740.【GaussDB(for MySQL)】 Big IN查询优化05-0941.GaussDB细粒度资源管控技术透视05-0942.带你了解GaussDB SQL中的BOOLEAN表达式05-1043.数仓安全:数据脱敏技术深度解析05-1044.详解GaussDB(DWS)中的行执行引擎05-1145.了解GaussDB SQL中CASE表达式05-1346.JDBC连接openGauss6.0和PostgreSQL16.2性能对比05-1447.MySQL 给用户添加 ALTER VIEW 的权限05-1548.MySQL全文索引源码剖析之Insert语句执行过程05-2049.全球厂商之最,华为17篇论文入选国际数据库顶会ICDE05-2250.GeminiDB PITR,让游戏回档“进退自如”!05-2451.浅析MySQL 8.0直方图原理05-2752.LLVM技术在GaussDB等数据库中的应用06-0353.告别内存OOM,解决MySQL内存增长问题06-0454.从数据库设计到性能调优,全面掌握openGemini应用开发最佳实践06-0455.深度体验与测评openGauss 6.0.0新版本06-1156.深度解读数据库引入LLVM技术后如何提升性能06-1257.从Purge机制说起,详解GaussDB(for MySQL)的优化策略06-1758.攀登不止,华为数据库论文入选SIGMOD 2024,技术创新再谱新篇06-1859.技术解读数据库如何实现“多租户”?06-2060.解读MySQL 8.0数据字典的初始化与启动06-2461.GeminiDB全面联动MySQL:热点数据,一键加速06-2662.探秘数据库中的并行计算技术应用07-0163.硬核解读,WeTune是如何提升数据库查询重写性能?07-0464.开源数据库Greenplu突然闭源?GaussDB(DWS)提供数仓新可能07-0865.数据库异常难定位?GaussDB(DWS)运维神器TopSQL来解决07-1066.MySQL派生表合并优化的原理和实现07-1167.华为云发起,openGemini正式成为CNCF官方项目!07-1168.MySQL中为什么要使用索引合并(Index Merge)?07-12
    69.解读MySQL 8.0数据字典缓存管理机制07-16
    70.解读GaussDB(for MySQL)灵活多维的二级分区表策略07-1971.深度解读GaussDB(for MySQL)与MySQL的COUNT查询并行优化策略07-25
    收起

    背景介绍

    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 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 
    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 *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 
    template 
    bool Shared_multi_map::get(const K &key, Cache_element **element) {
      Autolocker lock(this);
      *element = use_if_present(key);
      if (*element) return false;
    
      // Is the element already missed?
      if (m_map()->is_missed(key)) {
        while (m_map()->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()->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()->set_capacity(collation_capacity);
      instance()->m_map()->set_capacity(charset_capacity);
      ...
    }

    代码段5

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

    复制代码
    // Helper function to evict unused elements from the free list.
    template 
    void Shared_multi_map::rectify_free_list(Autolocker *lock) {
      mysql_mutex_assert_owner(&m_lock);
      while (map_capacity_exceeded() && m_free_list.length() > 0) {
        Cache_element *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的访问速度,显著的提升了数据库访问元数据信息的效率。另外本文还提到了元数据锁对数据字典缓存的保护,关于元数据锁的相关机制,会在后续文章陆续介绍。

     

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

     

  • 相关阅读:
    面向对象(一) 类和对象
    ESP32智能小车+PS2无线遥控器+麦克纳姆轮+microPython
    Vue2 零基础入门 Vue2 零基础入门第二天 2.4 vue的指令和过滤器 → 2.4.2 过滤器
    Flutter实战-请求封装(三)之http2
    【FPGA教程案例65】硬件开发板调试5——基于RS232的串口通信,由FPGA发射数据到PC
    LabView---双通道示波器(内含信号发生器)
    UEFI 安装 Debian12 Linux 物理机虚拟机VMware通用
    Invalid prop: custom validator check failed for prop “percentage
    系统调用理论详解,Linux操作系统原理与应用
    vue指令 (侦听器)
  • 原文地址:https://www.cnblogs.com/huaweiyun/p/18304461
  • 最新文章
  • 攻防演习之三天拿下官网站群
    数据安全治理学习——前期安全规划和安全管理体系建设
    企业安全 | 企业内一次钓鱼演练准备过程
    内网渗透测试 | Kerberos协议及其部分攻击手法
    0day的产生 | 不懂代码的"代码审计"
    安装scrcpy-client模块av模块异常,环境问题解决方案
    leetcode hot100【LeetCode 279. 完全平方数】java实现
    OpenWrt下安装Mosquitto
    AnatoMask论文汇总
    【AI日记】24.11.01 LangChain、openai api和github copilot
  • 热门文章
  • 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!!
    奉劝各位学弟学妹们,该打造你的技术影响力了!
    五年了,我在 CSDN 的两个一百万。
    Java俄罗斯方块,老程序员花了一个周末,连接中学年代!
    面试官都震惊,你这网络基础可以啊!
    你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法
    心情不好的时候,用 Python 画棵樱花树送给自己吧
    通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了!
    13 万字 C 语言从入门到精通保姆级教程2021 年版
    10行代码集2000张美女图,Python爬虫120例,再上征途
Copyright © 2022 侵权请联系2656653265@qq.com    京ICP备2022015340号-1
正则表达式工具 cron表达式工具 密码生成工具

京公网安备 11010502049817号