专栏首页程序员小灰什么是数据库的 “缓存池” ?(万字干货)

什么是数据库的 “缓存池” ?(万字干货)

1、Buffer Pool 概述

Buffer Pool 是什么?从字面上看是缓存池的意思,没错,它其实也就是缓存池的意思。它是 MySQL 当中至关重要的一个组件,可以这么说,MySQL的所有的增删改的操作都是在 Buffer Pool 中执行的。

但是数据不是在磁盘中的吗?怎么会和缓存池又有什么关系呢?那是因为如果 MySQL的操作都在磁盘中进行,那很显然效率是很低的,效率为什么低?因为数据库要从磁盘中拿数据啊,那肯定就需要IO啊,并且数据库并不知道它将要查找的数据是磁盘的哪个位置,所以这就需要进行随机IO,那这个性能简直就别玩了。所以 MySQL对数据的操作都是在内存中进行的,也就是在 Buffer Pool 这个内存组件中。

实际上他就好比是 Redis,因为 Redis 是一个内存是数据库,他的操作就都是在内存中进行的,并且会有一定的策略将其持久化到磁盘中。那 Buffer Pool 的内存结构具体是什么样子的,那么多的增删改操作难道数据要一直在内存中吗?既然说类似 redis 缓存,那是不是也像 redis 一样也有一定的淘汰策略呢?

本篇文章,会详细的介绍 Buffer Pool 的内存结构,让大家彻底明白这里面的每一步执行流程。我们先看一下 MySQL从加载磁盘文件到完成提交一个事务的整个流程。我们先来看一个总体的流程图,从数据在磁盘中被加载到缓存池中,然后经过一些列的操作最终又被刷入到磁盘的一个过程,都经历了哪些事情,这个图不明白没有关系,因为本文重点是 Buffer Pool 这个整体的流程就是让大家稍微有个印象。

2、Buffer Pool 有多大

Buffer Pool 是 InnoDB 中的一块内存区域,他一定是有自己的大小的,且大小默认是 128M,不过这个容量似乎有点小了,大家的自己的生产环境可以根据实际的内存大小进行调整,参数为:innodb_buffer_pool_size=2147483648 单位是字节,

他在 InnoDB 中的整体结构大概是这样子的

3、数据页

刚刚介绍到 MySQL在执行增删改的时候数据是会被加载到 Buffer Pool 中的,既然这样数据是怎么被加载进来的,是一条一条还是说是以其他的形式呢。我们操作的数据都是以表 + 行的方式,而表 + 行仅仅是逻辑上的概念,MySQL并不会像我们一样去操作行数据,而是抽象出来一个一个的数据页概念,每个数据页的大小默认是 16KB,这些参数都是可以调整的。但是建议使用默认的就好,毕竟 MySQL能做到极致的都已经做了。每个数据页存放着多条的数据,MySQL在执行增删改首先会定位到这条数据所在数据页,然后会将数据所在的数据页加载到 Buffer Pool 中。

4、缓存页

当数据页被加载到缓冲池中后,Buffer Pool 中也有叫缓存页的概念与其一一对应,大小同样是 16KB,但是 MySQL还为每个缓存也开辟额外的一些空间,用来描述对应的缓存页的一些信息,例如:数据页所属的表空间,数据页号,这些描述数据块的大小大概是缓存页的15%左右(约800B)。

5、Free链表

上面是说了每个数据页会被加载到一个缓存页中,但是加载的时候 MySQL是如何知道那个缓存页有数据,那个缓存页没有数据呢?换句话说, MySQL是怎么区分哪些缓存页是空闲的状态,是可以用来存放数据页的。

为了解决这个问题, MySQL 为 Buffer Pool 设计了一个双向链表— free链表,这个 free 链表的作用就是用来保存空闲缓存页的描述块(这句话这么说其实不严谨,换句话:每个空闲缓存页的描述数据组成一个双向链表,这个链表就是free链表)。之所以说free链表的作用就是用来保存空闲缓存页的描述数据是为了先让大家明白 free 链表的作用,另外 free 链表还会有一个基础节点,他会引用该链表的头结点和尾结点,还会记录节点的个数(也就是可用的空闲的缓存页的个数)。

这个时候,他可以用下面的图片来描述:

当加载数据页到缓存池中的时候, MySQL会从 free 链表中获取一个描述数据的信息,根据描述节点的信息拿到其对应的缓存页,然后将数据页信息放到该缓存页中,同时将链表中的该描述数据的节点移除。这就是数据页被读取 Buffer Pool 中的缓存页的过程。

但 MySQL是怎么知道哪些数据页已经被缓存了,哪些没有被缓存呢。实际上数据库中还有后一个哈希表结构,他的作用是用来存储表空间号 + 数据页号作为数据页的key,缓存页对应的地址作为其value,这样数据在加载的时候就会通过哈希表中的key来确定数据页是否被缓存了。

6、Flush链表

MySql 在执行增删改的时候会一直将数据以数据页的形式加载到 Buffer Pool 的缓存页中,增删改的操作都是在内存中执行的,然后会有一个后台的线程数将脏数据刷新到磁盘中,但是后台的线程肯定是需要知道应该刷新哪些啊。

针对这个问题,MySQL设计出了 Flush 链表,他的作用就是记录被修改过的脏数据所在的缓存页对应的描述数据。如果内存中的数据和数据库和数据库中的数据不一样,那这些数据我们就称之为脏数据,脏数据之所以叫脏数据,本质上就是被缓存到缓存池中的数据被修改了,但是还没有刷新到磁盘中。

同样的这些已经被修改了的数据所在的缓存页的描述数据会被维护到 Flush 中(其实结构和 free 链表是一样的),所以 Flush 中维护的是一些脏数据数据描述(准确地说是脏数据的所在的缓存页的数据描述)

另外,当某个脏缓存页被刷新到磁盘后,其空间就腾出来了,然后又会跑到 Free 链表中了。

7、LRU链表

如果系统一直在进行数据库的增删改操作,数据库内部的基本流程就是:

我们还拿 redis 类做类比,以便更好的帮助大家明白其原理。Flush 的作用其实类似 redis 的 key 设置的过期时间,所以一般情况下,redis 内存不会不够使用,但是总有特殊的情况,问题往往就是在这种极端和边边角角的情况下产生的。

如果 redis 的内存不够使用了,是不是自己还有一定的淘汰策略?最基本的准则就是淘汰掉不经常使用到的key。Buffer Pool 也类似,它也会有内存不够使用的情况,它是通过 LRU 链表来维护的。LRU 即 Least Recently Uesd(最近最少使用)。

MySql 会把最近使用最少的缓存页数据刷入到磁盘去,那 MySql 如何判断出 LRU 数据的呢?为此 MySql 专门设计了 LUR 链表,还引入了另一个概念:缓存命中率

说到这里,那LRU究竟是怎么工作的。假设 MySQL在将数据加载到缓存池的时候,他会将被加载进来的缓存页按照被加载进来的顺序插入到LRU链表的头部(就是链表的头插法),假设 MySQL现在先后分别加载A、B、C数据页到缓存页A、B、C中,然后 LRU 的链表大致是这样子的。

现在又来了一个请求,假设查询到的数据是已经被缓存在缓存页B中,这时候 MySQL就会将B缓存页对应的描述信息插入到LRU链表的头部,如下图:

然后又来了一个请求,数据是已经被缓存在了缓存页C中,然后LRU会变成这样子:

说到底,每次查询数据的时候如果数据已经在缓存页中,那么就会将该缓存页对应的描述信息放到LRU链表的头部,如果不在缓存页中,就去磁盘中查找,如果查找到了,就将其加载到缓存中,并将该数据对应的缓存页的描述信息插入到LRU链表的头部。也就是说最近使用的缓存页都会排在前面,而排在后面的说明是不经常被使用到的。

最后,如果 Buffer Pool 不够使用了,那么 MySQL就会将 LRU 链表中的尾节点刷入到磁盘中,用来给 Buffer Pool 腾出内存空间。来个整体的流程图给大家看下

8、LRU链表带来的麻烦

这里的麻烦指的是就是 MySQL本身的预读机制带来的问题

上图能够看到B的相邻也被加载到了C描述数据的前面,而实际上C的命中率比B的相邻页高多了,这就是LRU本身带来的问题。

还有一种情况是 SELECT * FROM students 这种直接全表扫描的,会直接加载表中的所有的数据到缓存中,这些数据基本是加载的时候查询一次,后面就基本使用不到了,但是加载这么多数据到链表的头部就将其他的经常命中的缓存页直接全挤到后面去了。

以上种种迹象表明,预读机制带来的问题还是蛮大的,既然这么大,那 MySQL为什么还要进入预读机制呢,说到底还是为了提高效率,**一种新的技术的引进,往往带来新的挑战**,下面我们就一起来看下 MySQL是如何解决预加载所带来的麻烦的。

9、基于冷热数据分离的LRU链表

所谓的冷热分离,就是将 LRU 链表分成两部分,一部分是经常被使用到的热数据,另一部分是被加载进来但是很少使用的冷数据。通过参数innodb_old_blocks_pct 参数控制的,默认为37,也就是 37% 。用图表示大致如下:

数据在从磁盘被加载到缓存池的时候,首先是会被放在冷数据区的头部,然后在一定时间之后,如果再次访问了这个数据,那么这个数据所在的缓存页对应描述数据就会被放转移到热数据区链表的头部。

那为什么说是在一定的时间之后呢,假设某条数据刚被加载到缓存池中,然后紧接着又被访问了一次,这个时候假设就将其转移到热数据区链表的头部,但是以后就再也不会被使用了,这样子是不是就还是会存在之前的问题呢?

所以 MySQL通过innodb_old_blocks_time来设置数据被加载到缓存池后的多少时间之后再次被访问,才会将该数据转移到热数据区链表的头部,该参数默认是1000单位为:毫秒,也就是1秒之后,如果该数据又被访问了,那么这个时候才会将该数据从 LRU 链表的冷数据区转移到热数据区。

现在再回头看下上面的问题

再来思考下 Buffer Pool 内存不够的问题

但是这样子还不是足够完美,为什么这么说,刚刚我们一直在讨论的是冷数据区的数据被访问,然后在一定规则之下会被加载到热数据链表的头部,但是现在某个请求需要访问的数据就在热数据区,那是不是直接把该数据所在的缓存页对应的描述数据转移到热数据区链表头部呢?

很显然不是这样子的,因为热数据区的数据本身就是会被频繁访问的,这样子如果每次访问都去移动链表,势必造成性能的下降(影响再小极端情况下也可能会不可控),所以 MySQL针对热数据区的数据的转移也有相关的规则。

该规则就是:如果被访问的数据所在的缓存页在热数据区的前25%,那么该缓存页对应的描述数据是不会被转移到热数据链表的头部的,只有当被访问的缓存页对应的描述数据在热数据区链表的后75%,该缓存页的描述数据才会被转移到热数据链表的头部

举个例子来说,假设热数据区有100个缓存页(这里的缓存页还是指的是缓存页对应的描述数据,再强调下,链表中存放的是缓存页的描述数据,为了方便有时候会直接说缓存页。希望朋友们注意),当被访问的缓存页在前25个的时候,热数据区的链表是不会有变化的,当被访问的缓存页在26~100(也就是数据在热数据区链表的后75%里面)的时候,这个时候被访问的缓存页才会被转移到链表的头部。

到此为止, MySQL对于LUR 链表的优化就堪称完美了。是不是看到这里瞬间感觉很多东西都明朗了,好了,对于 LRU 链表我们就讨论到这里了。

10、Buffer Pool 中的链表小结

后台线程将冷数据区的尾节点的描述数据对应的缓存页刷入磁盘文件中

11、Buffer Pool 的并发性能

我们平时的系统绝对不可能每次只有一个请求来访问的,说白了就是如果多个请求同时来执行增删改,那他们会并行的去操作 Buffer Pool 中的各种链表吗?如果是并行的会不会有什么问题。

实际上 MySQL在处理这个问题的时候考虑的非常简单,就是: Buffer Pool 一次只能允许一个线程来操作,一次只有一个线程来执行这一系列的操作,因为MySQL 为了保证数据的一致性,操作的时候必须缓存池加锁,一次只能有一个线程获取到锁

这个时候,大家这时候肯定满脑子问号。串行那还谈什么效率?大家别忘记了,这一系列的操作都是在内存中操作的,实际上这是一个瞬时的过程,在内存中的操作基本是几毫秒的甚至微妙级别的事情。

但是话又说回来,串行执行再怎么快也是串行,虽然不是性能瓶颈,这还有更好的优化办法吗?那肯定的 MySQL早就设计好了这些规则。那就是 Buffer Pool 是可以有多个的,可以通过 MySQL的配置文件来配置,参数分别是:

一般在生产环境中,在硬件不紧张的情况下,建议使用此策略。这个时候大家是不是又会有一个疑问(如果没有那说明你没认真思考哦),大家应该有这样的疑问:

12、动态调整 Buffer Pool 的大小

到此为止,本文已经详细的介绍了 Buffer Pool 的内存结构,它的数据是如何存放的,如何刷磁盘的,又是如何加载的,以什么样的形式存在的等等知识点,下面我们继续挖掘,将 Buffer Pool 的相关知识点一次说个够。我们现在来讨论下 Buffer Pool 的大小能否动态调整。

假设我们现在的 Buffer Pool 的大小是 2GB大小,现在想将其扩大到 4GB,现在说一下如果真的要这么做,我们的 MySq 需要做哪些事情。首先 ,MySQL 需要向操作系统申请一块大小为 4G 的连续的地址连续的内存空间,然后将原来的 Buffer Pool 中的数据拷贝到新的 Buffer Pool 中。

这样可能吗?如果原来的是8G,扩大到 16G,那这个将原来的数据复制到新的 Buffer Pool 中是不是极为耗时的,所以这样的操作 MySQL必然是不支持的。但实际上这样的需求是客观存在的,那 MySQL是如何解决的呢?

为了处理这种情况,MySQL设计出 chunk (http 协议中也有使用到这个思想,所以我们会发现很多技术的优秀思想都是在相互借鉴)机制来解决的

如果说有多个 Buffer Pool ,那就是这样

说到这里好像还是没有说到 MySQL到底是如何通过 chunk 机制来调整大小的。实际上是这样的,假设现在 Buffer Pool 有 2GB,里面有16个chunk,现在想要扩大到 4GB,那么这个时候只需要新申请一个个的 chunk 就可以了。

这样不但不需要申请一块很大的连续的空间,更不需要将复制数据。这样就能达到动态调整大小了(不会还有人问:这只是扩大,怎么缩小呢?gun)。不得不说 MySQL真机智。

13、生产环境如何设置 Buffer Pool 大小

Buffer Pool 是不是越大越好,理论上是的。那如果一个机器内存是16GB那分配给 Buffer Pool 15GB,这样很显然是不行的,因为操作系统要占内存,你的机器上总会运行其他的进行的吧?那肯定也是需要占用内存的。根据很多实际生产经验得出的比较合理的大小是机器内存大小的(50%~60%)。

最后一起来看看你的 INNODB 的相关参数,命令是show engine innodb status

14、结束语

本篇文章我们详细讨论了 Buffer Pool 的内存结构,从 free 链表到 lru 链表,从 Buffer Pool 到 chunk,从磁盘中加载一个数据页到 Buffer Pool 到最后该数据页又被刷回到磁盘中的一整个过程,他的每一步都做了什么。

我们一起讨论完本文以后,是不是瞬间有种看透来了 MySQL的感觉,但是这个仅仅是前提,学习这些的目的是为了更好的理解 MySQL让我们能够在工作中更加游刃有余地使用它。因为只有在知道了底层原理的情况下,才能熟悉他的工作原理,遇到问题才能对症下药。

—————END—————

本文分享自微信公众号 - 程序员小灰(chengxuyuanxiaohui),作者:坨坨

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-01-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【干货】小白学数据分析—留存率是什么?

    在网站分析、电商分析、网游分析中,对于留存率的关注度极高,这一浪潮随着APP应用、社交游戏的火爆逐渐成为一个很重要的衡量准则,也甚至有了40-20-10准则。对...

    CDA数据分析师
  • 数据库连接池到底应该设多大?

    我在研究HikariCP(一个数据库连接池)时无意间在HikariCP的Github wiki上看到了一篇文章,这篇文章有力地消除了我一直以来的疑虑,看完之后感...

    用户1516716
  • 数据库原理: Change Buffer 是干什么的?

    redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的是随机读磁盘的IO消耗。

    王小明_HIT
  • 面试问烂的 MySQL 查询优化,看完屌打面试官!

    https://segmentfault.com/a/1190000013672421

    Java技术栈
  • 后端概述:原子-硬件-分布式/集群 - 新设想

     一开始可能只是一个用户或者几个用户访问,但是产品放出去总是要面向社会的,随着用户越来越多,首先要解决的是正确地执行我写的业务逻辑。

    执生
  • Nginx引入线程池 性能提升9倍

    正如我们所知,NGINX采用了异步、事件驱动的方法来处理连接。这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作...

    哲洛不闹
  • 什么是MySQL数据库?看这一篇干货文章就够了!

    为啥学习MySQL呢?因为MySQL是最流行的关系型数据库管理系统之一,在web应用方面,MySQL是最好的软件。MySQL所使用的sql语言是用于访问数据库的...

    达达前端
  • 达到年薪 40W 必需掌握的技术。

    很多人在问我,程序员如何拿高薪,如何做到年薪40W+,其实总结出来还是一句话,你的技术决定你的能力已经薪资。

    Java技术栈
  • NGINX引入线程池 性能提升9倍

    正如我们所知,NGINX采用了异步、事件驱动的方法来处理连接。这种处理方式无需(像使用传统架构的服务器一样)为每个请求创建额外的专用进程或者线程,而是在一个工作...

    流柯
  • “上车”之前先系好安全带——数字货币入门基础指南(一)

    不管你有没有关注数字货币,只要你平时看新闻,“比特币”一定出现在你的视野中。随着比特币成功突破2万美金,半年实现4倍的增值,比特币被推上风口浪尖之后,12月的数...

    企鹅号小编
  • 专业解决 MySQL 查询速度慢与性能差

    QPS: QueriesPerSecond意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准...

    搜云库技术团队
  • 一次性能优化:吞吐量从1提升到2500

    性能优化,简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。压测也是检验一个架构设计是否合理的一个重要方法。

    搜云库技术团队
  • 漫漫优化路,总会错几步!记一次接口优化!

    点击上方"IT牧场",选择"设为星标"技术干货每日送达!来源:www.cnblogs.com/cjsblog/p/10573215.html

    用户1516716
  • 后端服务慢成狗?试试这 7 招!

    优化代码实现是第一位的,特别是一些不合理的复杂实现。如果结合需求能从代码实现的角度,使用更高效的算法或方案实现,进而解决问题,那是最简单有效的。

    Java技术栈
  • 让你的硬盘速度快40倍

    废话不多说 直接开干 原理:内存做硬盘的缓存。数据先进入缓存中,之后再延迟写入,机械硬盘使用提升最明显。 优缺点:不推荐台式机这样弄,因为他没有电池,他没有电池...

    Erwin
  • 比特币和区块链是什么?看完这篇文章不再懵

    实际上全球各种虚拟货币已经超过了1300种,其中市值排名前几名的除了比特币还有下面的这几种虚拟货币。

    進无尽
  • 面试官:能说一说Mysql缓存池吗?

    狂聊君:啊,这么难吗,容我组织一下语言。(内心OS:这TM还不简单?我能给你扯半小时!)

    肉眼品世界
  • 大数据如何助力新零售起飞,把商业机密讲给你听!

    世界上有千千万万种的零售企业,它们的商业模式各不相同,有的持续赚钱增长,有的却亏损倒闭。那么,零售行业如何赚钱,它们的底层逻辑是什么,大数据又如何助力新零售,今...

    木东居士
  • 后台开发常问面试题集锦(问题搬运工,文末附问题链接)

    Synchronized(对象锁)和Static Synchronized(类锁)的区别

    Java架构技术

扫码关注云+社区

领取腾讯云代金券