MySQL中的Buffer Pool

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 介绍MySQL中的Buffer Pool中的知识

1、Buffer Pool是什么

Buffer Pool是MySQL中非常重要的一个组件,众所周知,MySQL的数据实际是放在磁盘上面的,但是我们在对数据库进行增删改查操作的时候,是不可能直接去操作磁盘上面的数据的,因为你如果去直接操作磁盘上面的数据,那速度会相当的慢,对于要求支持大量并发的数据库来说,这是无法接受的。


所以MySQL在为了提升处理速度,并不会去直接操作磁盘的数据,所以不操作磁盘,就只能操作内存了,内存的速度一般都是相当快的,这样就可以提升数据库的并发量,那么数据是在磁盘的,要在内存操作,必然要将数据从磁盘加载到内存之中,那加载到内存当中,肯定也要按照一定的结构来存放数据,这样才便于对数据去进行操作,那么这个存放被加载的数据的内存结构就是Buffer Pool。

2、Buffer Pool有什么作用

Buffer Pool既然是一块内存,MySQL操作数据库的增删改查都是直接操作的内存,那么就可以知道,我们日常的增删改查其实都是在操作MySQL的Buffer Pool,每次对数据的修改,其实都是修改的内存中的数据,也就是Buffer Pool里面的数据,数据库中真实的在磁盘上面的数据并没有得到修改,那么这样肯定会导致内存和磁盘数据的不一致,所以我们猜想,数据库肯定有相应的机制,会将数据库在Buffer Pool里面的数据再去更新到MySQL的真实磁盘数据里面去。


所以Buffer Pool的作用就是在数据库操作者和数据库真实数据文件之间,起到了一个提升操作速度的缓存作用,因为操作内存总是会比操作磁盘速度要快。

3、Buffer Pool内存数据结构

既然Buffer Pool是一块数据库用来提高自己并发的内存数据结构,那么这个内存数据结构,是长什么样的呢?


首先我们来看一下这个Buffer Pool到底有多大,既然它是一个内存数据结构,那么这块内存肯定是有大小的,不会无限大,毕竟内存也是有限的。那它会有多大呢?


Buffer Pool默认是128MB,默认就是这么大,好像也存不了多少数据,那能不能进行修改呢?当然是可以的,我们只要修改innodb_buffer_pool_size参数就可以了,参数的值单位为B,所以128M的配置信息为inndob_buffer_pool_size=(128 * 1024 *1024) = 134217728。通过这个我们就可以指定这个buffer pool的大小。


练习一下,如果我们要将size配置为2G,那么值应该是多少呢?我们只需要打开计算器计算一下就在知道了嘛,2GB = 2 1024MB = 2 * 1024 1024 KB = 2 1024 *1024 *1024 B = 2147483648。


接下来我们在看看Buffer pool到底长啥样,我们现在知道了Buffer Pool是MySQL的一个组件,而且是在内存中,那么我们就可以画出第一张图,描述他们之间的关系。

那么我们接着看,这个Buffer Pool里面到底是什么样的,众所周知数据库的核心数据模型就是表 + 行 +字段组成的,也就是说数据库是由一张纸表组成的,一张表又是由一行行数据组成的,一行数据又可以拆成若干个字段,所以数据在Buffer Pool里面会不会也是这么放的呢?


其实事实并不是这样的,实际上MySQL对数据抽象出来了一个数据页的概念,什么是数据页,就是很多行数据的一个集合,一个数据页里面包含了很多个数据行,所以磁盘文件会对应很多个数据页,一个数据页大小是16KB。也就是说Buffer Pool里面存放的并不是一行行的数据,而是一页一页的数据,这个页就是数据页。

那么磁盘上面的数据页和Buffer Pool中的缓存页是如何对应起来的


我们知道一个数据页的大小是固定的16KB,Buffer Pool的大小默认是128MB,而且还可以在进行修改,这也就是说Buffer Pool里面肯定会存在很多个数据页,数据页在被加载到Buffer Pool中之后,我们习惯上称其为缓存页。


这么多的缓存页肯定要和数据页相对应,我们可以猜想肯定会有一部分的信息是描述这个关联的,对吧。实际上正如我们所料,MySQL也确实是如此的。对于每个缓存页,它实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓存页的。


比如包含如下的一些信息:这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址以及其他的一些信息,每个缓存页都会对应一个描述信息,这个描述想你想本身也是一块数据,这Buffer Pool 里面,这个缓存页的描述数据放在最前面,然后各个缓存页放在后面,此时,Buffer Pool看起来就是下面这个样子。

而且需要注意一点,Buffer Pool中的描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大约是800个字节左右的大小,然后假设你设置的Buffer Pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能会有130多MB的样子,因为它里面还要存放每个缓存页的描述数据。

4、如何知道那个数据页是空闲的---> free链表登场

我们先来看一个问题。既然Buffer Pool是用来存放数据页的,那么它的结构肯定要适应存放多个数据页的需求,一块内存在使用之前肯定要进行初始化操作,那么数据库在启动的时候,是如何初始化这个Buffer Pool的。


这个其实也是很简单的,只要数据库一启动,就会按照你设置的Buffer Pool大小,稍微再加大一下(用来存放描述信息),去找操作信息申请一块内存区域,作为Buffer Pool的内存区域,然后当内存区域申请完毕之后,数据库就会按照默认的缓存页16KB大小以及对应的800个字节左右的描述信息的大小,在Buffer Pool中划分出来一个一个的缓存页个一个一个的它们对应的描述数据,只是这个时候,Buffer Pool中的一个一个的缓存页都是空的,里面什么也没有,要等数据库运行起来之后,当我们对其进行增删改查操作的时候,才会把数据对应的数据页从磁盘文件里面读出来,放入Buffer Pool中的缓存页中去。


当数据运行起来之后,肯定会不停的执行增删改查操作,此时就需要不停的从磁盘上面读取一个一个的数据页放入Buffer Pool中的对应的缓存页里面去,把数据缓存起来,然后就可以在内存中对这个数据进行增删改查操作了。 但是此时在从磁盘上读取数据页放入Buffer Pool中的时候,必然会涉及一个问题,那就是那些缓存页是空的?


所以数据库为了解决这个问题,为Buffer Pool设计了一个free链表,它是一个双向链表,在这个free链表里,每个节点就是一个空闲的缓存页的描述数据块的地址,也就是说,只要你一个缓存页是空的,那么它的描述数据块就会被放入这个free链表当中。刚开始数据库启动的时候,可能所有的缓存页都是空闲的,那么此时可能是一个空的数据库,一条数据也没有,所以此时所有的缓存页的描述数据块,都会被放入这个free链表中。就会出现下面这样一个图。

这个free链表里面就是各个缓存页的描述数据块,只要缓存页是空闲的,那么他们对应的描述数据块就会加入到这个free链表里面去,每个节点都会双向链接自己的前后节点组成一个双向链表。除此之外,这个free链表还有一个基础节点,它会引用头节点和尾节点,里面存储了链表中有多少个描述数据块的节点,也就是还有多少个空闲的缓存页。


既然他是由描述数据块组成的链表,那么free链表占用多少空间呢?

可能会有人认为这个描述数据库,在Buffer Pool里面有一块份,在free链表里面也有一份,好像在内存中会有两个一模一样的描述数据块,你也是这么认为的吗?


如果你也是这么想,那就错了,本着节约资源、能重复利用就重复利用、能不浪费就不浪费的原则,free链表是不会再去搞一个一模一样的描述数据块出来的;这个所谓的free链表,其实它本身就是由Buffer Pool里的数据描述块组成的,你也可以认为是每个描述数据块都有两个指针,一个是free_pre指针,指向ree链表的自己的上一个节点,一个是free_next指针,指向free链表的自己的下一个节点。通过这两个指针,就可以把Buffer Pool中的描述数据块串成一个链表。


对于free链表而言,只有一个基础节点是不属于Buffer Pool的,它是40字节大小的一个节点,里面存放了free链表的头节点的地址、尾节点的地址、还有free链表里当前有多少个节点。


如何将磁盘上面的数据读取到Buffer Pool的缓存页里面去?

既然我们已经有了free链表,那么这个问题就简单了,首先我们需要从free链表中获取一个数据描述块,然后就可以对应的获取到这给描述数据块对应的空闲缓存页,接着就可以把磁盘上的数据页读取到对应的缓存页里面去了,同时把相关的一些描述数据写入缓存页对应的描述数据块里面去,最后把这个描述数据块从free链表中去除就可以了。


所以怎么装数据,分三步,第一步获取一个空的缓存页,第二步把数据放入这个缓存页,第三步把这个缓存页从free链表中删除,欧耶。


怎么知道数据页有没有被缓存?

这样我们就会又遇到一个问题,就是你拿到了一个数据页,你怎么知道这个数据页是不是以及被缓存到了Buffer Pool中的缓存页里面了,毕竟我不可能每次都缓存吧?这样那还有什么意义呢。


我们在指向增删改查操作的时候,肯定首先要看看这个数据页有没有被缓存,如果没有缓存就走上面的三步把数据放进去,如果已经被缓存了,那就应该直接拿出来使用。那么我怎么知道有没有被缓存呢?


所以数据库为了解决这个问题,其内部维护了一个哈希表数据结构,它会用表空间号+数据页号,作为一个key,然后缓存页的地址作为value,当要使用一个数据页的时候,通过 表空间号+数据页号 作为key去这个哈希表里查询一下,如果没有就读取放进去,如果已经有了,就说明数据页已经被缓存了,获取到数据页的地址,直接使用即可。

也就是说,每次读取一个数据页到缓存之后,都会在这个哈希表中写入一个key-value对,key就是 表空间号+数据页号,value就是这个缓存页的地址,那么下次如果你在使用这个缓存页,就可以直接从哈希表中获取这个已经被缓存的缓存页即可。


5、内存中修改了数据,如何知道我修改了那个缓存页---> flush链表登场

接下来我们看一个很关键的问题,那就是在执行增删改查的时候,如果发现数据页没被缓存,那么必然会基于free链表找到一个空闲的缓存页,然后将数据从磁盘加载到缓存页里面去,但是如果一次缓存了,那么下次就必然会直接使用缓存页。


所以不管怎么样,要进行操作的数据都会在Buffer Pool里面,接着肯定会去更新Buffer Pool的缓存页中的数据,此时一旦更新了缓存页中的数据,那么缓存页里的数据就和磁盘中的数据页里面的数据不一致了,这个时候我们就说缓存页是脏数据、脏页。


那么我们怎么知道那些缓存页是脏页呢?

我们知道这些在内存里面更新的脏页的数据,最终都是要被刷回磁盘的;这样就出现了一个问题,我不可能把所有的缓存页都刷回磁盘吧,那些没有被修改过的数据刷回磁盘也没有什么意义,反而会浪费时间,降低数据库的性能,这简直就是腿裤子放屁-多此一举,


所以我必须要能知道那个缓存页确实被我修改了,为了解决这个问题,数据库引入了一个新的链表---flush链表,flush链表和free链表类型,都是一个双向链表,而且其本质也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的数据描述块组成一个双向链表。这样凡是修改过的缓存页,都会把它的描述数据块加入到flush链表里面去,后续只要刷新flush链表中的这样缓存页到磁盘即可。


6、当Buffer Pool缓存页不够用的时候怎么办?

俗话说,一尺之捶,日取其半,万世不竭;但是我们的Buffer Pool可不是一半一半的取,它总是会不够用的。


我们前面已经知道了Buffer Pool会将空闲的缓存页组织成一个free链表,那么如果你有一个新的增删改查需求过来,如果这次增删改查的数据对应的那个数据页没有被缓存到Buffer Pool里面,那么它肯定要被加载到Buffer Pool里面去,这样就必须要到free链表里面去找一个空闲的节点,但是我们说一个默认的Buffer Pool默认大小是128M,一个缓存页也就是数据页的大小为16K,那么我们就可以知道这么大的一个Buffer Pool最多只能放(128 * 1024 / 16) = 8192个缓存页,那么万一放完了怎么办?


俗话说,旧的不去,新的不来,一个萝卜一个坑,要想放入新的,肯定就要淘汰旧的,只有让旧的让出位置,才能让新的进入,但是淘汰谁呢?掌心掌背都是肉,不能随意淘汰吧,总的有一套标准和规则。就像蜻蜓队长一样,对于和平星也不能想给谁就给谁,还是得按照一定的规则来决出一个胜负。我们缓存页的淘汰也一样,也要有属于自己的一套规则,谁不符合规则谁就淘汰,就是这么简单。那么规则是什么呢?


首先先考虑一下怎么淘汰一个缓存页呢,我们知道缓存页的数据在内存中经历了增删改一系列的操作,它此时是一个脏页,要把它从Buffer Pool里面给清空,那我们肯定要先把它的数据刷新会磁盘吧;所以还是三步,第一步,把数据刷回磁盘里面去,第二步,把自己重新加入到free链表里面去,第三步,把新的数据放到空闲的缓存页,并把自己从free链表里面删除掉。


缓存命中率

假设我们现在有两个缓存页,一个缓存页数据,经常会被修改和查询,比如100次操作当中,有30次都是操作这个缓存页的数据,那么我们就可以说这个缓存页的命中率是很高的;相反另一个缓存页呢,可能就被第一次加载进来的时候使用了一次,然后在这100次操作中从来都没有被使用过,那么我们就可以说这个缓存页的命中率有点低。


那么这样的两个缓存页,如果现在必须要淘汰一个,那你会选择淘汰哪一个呢? 本着二八法则、马太效应、不吃凉粉腾板凳,我们肯定会选择淘汰第二个缓存页,也就是那个根本没人访问的缓存页,因为第二个缓存页根本就没有什么人来使用它嘛,结果它还占用了一个缓存页,这不就是占着茅坑不拉屎 ,所以必须淘汰它。


引入LRU链表来判断那些缓存页是不被经常使用的

既然标准我们已经有了,按照缓存命中率来,那我们就必须要知道每个缓存页的命中率了,也不用知道具体的值,只要知道所有缓存页的命中率的一个排名,直接淘汰最低的那一部分不就可以了,那么那个数据结构能满足这个呢,


让我们想一想我们当初学的操作系统,它是怎么来进行这种内存淘汰的呢,好像岂有LFU(最少使用淘汰)算法、FIFO(先进先出淘汰)算法、OPT(最佳置换)算法、LRU(最近最少使用)算法,那我们直接从里面选择一种不久可以了吗,那么选哪一个呢?


我们需要保留那个最常用的,FIFO肯定不适合,OPT是理想算法不能实现,LFU只能保证最少,如果一个我们最近刚开始频繁使用,但是它的次数还没有太多,那么使用LFU会被淘汰,但是它是不应该被淘汰的,所以最后只剩下了LRU(Least Recently Used)最近最少使用算法。


有了这个LRU链表,我们就可以知道那些缓存页是最近最少被使用的,那么当我们没有缓存页可用而必须要淘汰一部分时候,我们就可以选择这个LRU链表中最近最少使用的缓存页(也就是链表尾部的那些节点)进行淘汰即可。


LRU的工作原理:新加载的节点,把它放在LRU链表的最前面,这样就可以保证最近操作的节点不会被删除,如果有一个已经在LRU链表中的节点被操作了,那么酒吧这个节点移动到LRU链表的最前面,这样就可以保证最近最长使用的节点永远在链表的头部,而尾部的肯定是不经常被使用的节点,这样需要淘汰的时候,直接将其淘汰即可。


7、简单的LRU链表在Buffer Pool中会导致什么问题?

预读

首先我们来解释一个概念,预读;什么是预读呢,预读就是说当你从磁盘上加载一个数据页的时候,它可能会连带这把这个数据页相邻的其它数据页,页加载到Buffer Pool里面去,为什么这么做呢,因为一次IO的代价是很高的,既然这样索性一次多弄点数据,减少IO的次数,提升一定的性能,但是这样就给LRU链表带来了一个巨大的问题。


预读带来的问题

我们知道新加载的数据,会被放在LRU链表的最前面,最前面的节点是不会被淘汰的,所以假设我们现在有两个空闲的缓存页,然后加载一个新的数据页的时候,连带这把它的一个相邻的数据页也加载了进来,这样两个数据页占用了两个缓存页,正好满足要求,但是接下里呢,实际了只要前面的那一个缓存页被访问了,另一个通过预读机制被加载进来的缓存页根本就无人问津,但是此时它却在链表的最前面,如图所示

我们来看这样一个场景,缓存页3已经被访问了10次,缓存页4已经被访问了8次,然后来了一个新的请求,这个请求需要的数据页不在Buffer Pool中,这时候就需要从磁盘将其加载到Buffer Pool的一个对应的缓存页里面去,在加载的时候通过预读的机制,同时还加载了数据页2到缓存页2里面去,那么这个时候缓存页2其实是没有被任何人访问过的,但是它却到了LRU链表的头部位置去了,这时候如果要淘汰相应的数据,我们应该要淘汰缓存页2才对,但是由于其在LRU链表的最前端,会导致我们无法淘汰这个根本站着茅坑不拉屎的家伙,反而淘汰了一个最近大量被使用的缓存页,这样是完全不合理的。


预读机制的触发条件

预读机制加载进来的数据可能根本就是无人问津的数据,但是他却占用了缓存页的空间,而且还在前部,无法及时将其淘汰,所以我们不应该随意去加载一些不相关的数据页进来,所以必须对其进行一定的限制,只有满足一定的条件才能启用这个预读的机制,那么有哪些条件呢? 数据块设计了两个条件来触发这个预读机制。


(1)、数据块有一个配置参数innodb_read_ahead_threshold,它的默认值为56,表示的含义为如果顺序的访问了一个数据区里面的多个数据页,这个数量超过了这个参数的值,此时就会触发预读机制,把下一个相邻数据区中的所有数据页都加载到Buffer Pool对应的缓存区里面去。


(2)、如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁被访问的,此时就会直接触发预读机制,把这个数据区里的其他数据页都加载到Buffer Pool里面去。但是这个机制是通过innodb_random_read_ahead参数来进行控制的,其默认为OFF,也就是默认这个规则是关闭不起效的。


另外一种可能导致频繁被访问的缓存页被淘汰的场景

还有一种可能会导致频繁被访问的数据页被淘汰的场景,那就是全表扫描


全表扫描就是说查询的sql语句没有任何的where条件,导致直接一下把这个表里的所有数据页都加载到了Buffer Pool里面去,这样就会导致,LRU链表前面的一大部分缓存页都是通过全表扫描进入的,但是这些缓存页又同时是无人问津的,而此时LRU链表的尾部可能才是最近被频繁使用的那些缓存页,但是如果这是进行淘汰,就会导致把那些最近最常使用的缓存页被淘汰掉,这也是极不合理的。


8、基于冷热数据分离的思想设计LRU链表

为了解决我们上面提出的问题,真正MySQL在设计LRU链表的时候,实际上采取的是冷热数据分离的思想。之前的预读、全表扫描导致的问题,归根结底都是因为所有的缓存页,不管用的不用的都在同一个链表里面才导致我们无法去分辨,那么我们将其拆开不就行了吗,恭喜你,MySQL也是这么想的。


所以真正的LRU链表,会被拆分为两个部分,一部分是热数据、一部分是冷数据,这个冷热数据的比例是通过innodb _old_blocks_pct这个参数来进行控制的,它的默认值为37,也就是说冷数据占比为37%,相应的热数据就占63%。

如何解决预读和全表扫描导致的问题--->如何解决第一次数据加载的问题

既然LRU链表已经通过一定的比例分成了冷、热两块区域,那么接下来我们就来看一下,这两部分如何相互转化和使用。 首先数据在第一次加载到Buffer Pool的时候,缓存页会被放在冷数据区域的头部,这样将保证了预读和全表扫描进来的这样数据页是在整个LRU链表的尾部,而不会到头部,那么冷数据区域的数据如果被操作了会怎么样呢?


我们可能会有这样的想法,如果冷数据区域的缓存页进行了一次访问,那就把它移动到热数据区域的头部去,这样行不行呢?其实这样显然也是不太合理的,比如济钢加载了一个数据页到缓存页,这时候这个缓存页在冷数据的链表头部,然后马上就被访问了一下,但是之后就再也没有被访问过了,那这样的缓存页放到热数据的头部又有什么意义呢。


所以MySQL设定了一个规则,其设计了一个innodb_old_blocks_time参数,其默认值为1000,单位为毫秒,也就是说,一个数据页被加载到缓存页之后,必须要在1000ms之后这个缓存页被访问了,才会把它挪动到热数据区域的链表头部去,这样就可以避开这种加载了里面被访问一次,然后就无人问津的这个缓存页被加载到最前面,防止其占着茅坑不拉屎。


LRU热数据区域的优化

我们知道在热数据区域,如果一个缓存页被访问了,这个缓存页会被移动到热数据LRU链表的头部去,但是你要知道,热数据区域里的缓存页可能都是被经常访问的,所以其移动如果太过于频繁也不是太好,也没有这个必要,所以MySQL对其进行了一定的优化。


MySQL指定了一个相应的规则,如果被访问的数据页在热数据区域的前1/4位置以内,它是不会移动到链表的头部去的,如果被访问的数据页在热数据区域的后3/4位置以内,才会将其移动到链表的头部去;这样一来就可以尽可能的减少链表中的节点移动,提升MySQL的性能。


9、LRU链表中尾部的缓存页,如何将其刷入磁盘

定时把LRU尾部的部分缓存页刷入磁盘

首先第一个时机,并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台线程,它会运行一个定时任务,这个定时认为每隔一段时间就会把LRU链表的冷数据区的尾部的一些缓存页输入磁盘里面去,清空这个缓存页,将其加入到free链表里面去。所以实际上可能你的缓存页根本就还没有放满,可能就会清空一些缓存页了。


所以缓存页的变化是一个动态的变化,其中的三个链表也在不断的进行着动态的变化,当你就一个数据页加载到缓存页的时候,首先free链表回去删除这个缓存页节点,LRU链表会加入这个缓存页节点,你修改了这个缓存页的值,可能LRU链表中这个缓存页节点的顺序也会发生改变,同时幽会把这个缓存页节点加入flush链表里面去,然后其可能又会被定时回收,回收时其会被清空,flush链表会将其删除、LRU链表也将其删除,然后其又会重新加入free链表里面去,所以整个过程是一个动态的变化过程。


把flush链表中的一些缓存页刷入磁盘

如果仅仅只是通过定时任务去吧冷数据区域的缓存刷入磁盘,这样也是不够的,因为在内存中被频繁修改的缓存页是在热数据区域的,这些被修改的数据是肯定要被输入磁盘的,总不能一值就在Buffer Pool里面待着吧。


所以这个后台线程同时也会在MySQL不怎么繁忙的时候,找个时间将flush链表中的缓存页都刷入磁盘,然后将这些缓存页加入到free链表里面去待用。


如果实在没有缓存页可用来怎么办?

这个时候如果要从磁盘加载数据页到一个空闲的缓存页中去,此时就会从LRU链表的冷数据区域的尾部找一个缓存页,将其输入磁盘清空,然后将新的数据页加载到其中去。


10、Buffer Pool的并发问题

Buffer Pool在访问的时候需要加锁吗?

现在我们已经知道了Buffer Pool其实本质就是一块大内存数据结构,由一大堆的缓存页和描述数据块组成,然后在加上各种链表(free、flush、lru)来辅助其运作。


那么假设MySQL接收到了多个请求。其就会用多个线程来处理这多个请求,每个线程会负责处理一个请求,然后这多个线程会同时去访问Buffer Pool,同时去操作里面的缓存页、同时擦操作同一个链表,这简直就是灾难啊。


多线程并发访问加锁,数据库的性能还能好吗?

应该这么说,即使就一个Buffer Pool,即使多个线程会加锁串行着排队执行,其实性能也差不到哪儿去。 因为大部分情况下,每个线程都是查询或者更新缓存页里的数据,这个操作是发生在内存里的,基本都是微秒级的,很快很快,包括更新free、flush、lru这些链表,他因为都是基于链表进行一些指针操作,性能也是极高的。


所以即使每个线程排队加锁,然后执行一系列操作,数据库的性能倒也是还可以的。 但是再怎么可以,你毕竟也是每个线程加锁然后排队一个一个操作,这也不是特别的好,特别是有的时候你的线程拿到锁之后,他可能要从磁盘里读取数据页加载到缓存页里去,这还发生了一次磁盘IO呢!所以他要是进行磁盘IO的话,也许耗时就会多一些,那么后面排队等他的线程自然就多等一会儿了! 所以对应Buffer Pool的操作是必然需要加锁的。


多个Buffer Pool优化并发能力

既然操作Buffer Pool是要加锁的,那我们能不能搞多个Buffer Pool呢,这样只要是操作的缓存页在不同的Buffer Pool里面,不久可以同时进行操作了,很高兴,这样是可以的。


一般来说,MySQL的默认规则是如果你给Buffer Pool分配的内存小于1G的话,那么最多就只会给你一个Buffer Pool。


但是如果你的机器内存足够大,你就可以给Buffer Pool分配较大的内存,比如给它个8G,那么这时候你就可以同时设置都给Buffer Pool了,怎么设置呢,秉着万物皆可配置的原则,肯定是修改配置参数了,这里有两个参数,innodb_buffer_pool_size这参数设置Buffer Pool的总大小,单位为B,innodb_buffer_pool_instance这个参数设置Buffer Pool的个数,比如如下配置 innodb_buffer_pool_size = 8589934592 innodb_buffer_pool_instances = 4。


我们给buffer pool设置了8GB的总内存,然后设置了他应该有4个Buffer Pool,此时就是说,每个buffer pool的大小就是2GB。这个时候,MySQL在运行的时候就会有4个Buffer Pool了!每个Buffer Pool负责管理一部分的缓存页和描述数据块,有自己独立的free、flush、lru等链表。


这个时候,假设多个线程并发过来访问,那么不就可以把压力分散开来了吗?有的线程访问这个buffer pool,有的线程访问那个buffer pool。 所以这样的话,一旦你有了多个buffer pool之后,你的多线程并发访问的性能就会得到成倍的提升,因为多个线程可以在不同的buffer pool中加锁和执行自己的操作,大家可以并发来执行了!


所以这个在实际生产环境中,设置多个buffer pool来优化高并发访问性能,是mysql一个很重要的优化技巧。


11、如何通过chunk来支持数据库运行期间的Buffer Pool动态调整?

Buffer Pool能在运行期间动态调整大小吗

按照我们目前的理论来说,Buffer Pool在运行期间肯定是不能动态调整大小的。 因为动态调整buffer pool大小,比如buffer pool本来是8G,运行期间你给调整为16G了,此时是怎么实现的呢?


就是需要这个时候向操作系统申请一块新的16GB的连续内存,然后把现在的buffer pool中的所有缓存页、描述数据块、各种链表,都拷贝到新的16GB的内存中去。这个过程是极为耗时的,性能很低下,是不可以接受的!


所以就目前讲解的这套原理,buffer pool是绝对不能支持运行期间动态调整大小的。


如何基于chunk机制把Buffer Pool 给拆小

既然我们说按照我们现在的原理,是没法在运行期间进行动态调整大小的,但是MySQL却是可以的,那么MySQL是通过什么机制来达到这种效果的呢,实际上就是源于Buffer Pool采用了chunk机制的设计,也就是说MySQL的Buffer Pool并不是一块完整的内存,而是很多块小内存组合而来的,而这一个个组成Buffer Pool的小内存块,我们就称其为chunk。


chunk的大小是通过innodb_buffer_pool_chunk_size参数来决定的,其默认值为128MB。 比如我们现在给Buffer Pool设置了一个8GB的总大小,然后将其分为了4个Buffer Pool,那么每个Buffer Pool的大小就是2GB,既然一个Buffer Pool是由若干个大小为128MB的chunk组成的,那么我们就可以得知,这个Buffer Pool是通过 2 * 1024 / 128 = 16个chunk组成的。也就是说这2GB的内存并不是一个连续完整的内存块,而是16个128MB的内存块组成的。


然后每个Buffer Pool里面的每个chunk里面就是一系列的描述数据块和缓存页,每个Buffer Pool里面的多个chunk共享一套free、flush、lru链表,所以正真的Buffer Pool的结构应该是下面这样的。

基于chunk机制是如何在运行期间支持调整Buffer Pool大小的

现在有了这套chunk机制,那么我们就可以在数据块运行期间,就可以支持动态调整Buffer Pool大小了。


我们前面说了,动态调整大小的问题在于两点,第一点就是可能无法获得一份完整的连续大内存区域,第二点就是你如果要拷贝一份这么大的内存的数据是及其耗时的,那么chunk机制是如何解决这些问题的呢?


对于第一点,我们知道了Buffer Pool是由一系列的chunk机制组成的,那么我们动态调整大小的时候,并不需要去获取一块连续的大内存区域,只需要获取一些128MB的chunk即可,把这些申请到底chunk分配给这个Buffer Pool即可;对于第二点,既然不需要获取完整连续的内存,那也就是说数据时不要迁移的,还是放在原本的chunk里面,完美的避免了数据的迁移问题。


12、如何基于机器配置来合理设置Buffer Pool的大小

应该给Buffer Pool设置多少内存

既然我们现在有了一台机器,比如说32GB,那么我们给Buffer Pool设置多少内存的,首先我们肯定知道Buffer Pool越大越好,那么直接给其30GB,这样数据块的大量操作都是基于内存的,性能肯定好,那么这样可以吗?


这样肯定是不行的,为什么呢?因为你的操作系统也要占用内存啊,机器上面的其他应用也要占用内存,就连MySQL也需要其他的一些内存,所以这样是不可以的,如果你胡乱给其设置一个特别大的内存给Buffer Pool,有可能会导致MySQL启动失败,因为其启动的时候内存根本就不够用。


所以通常来说,设置给Buffer Pool的内存应该在机器内存的50%~60%左右,这就是一个比较合理、健康的比例。


Buffer Pool总大小=(chunk大小 * Buffer Pool数量)的倍数

确定了Buffer Pool的总大小之后,就要考虑chunk的大小和Buffer Pool的数量了,此时要记住,有一个很关键的公式就是:buffer pool总大小=(chunk大小 * buffer pool数量)的倍数。


比如默认的chunk大小为128MB,假如你的Buffer Pool的总大小为20GB,那么你得算一下,此时Buffer Pool的数量应为多少? 通过上面的公式我们可以计算得知,Buffer Pool的数量= Buffer Pool总大小 / chunk大小,即20GB / 128MB = 160,然后根据其为一个倍数, 也就是说你可以设置Buffer Pool的个数为160的一个月数即可。


比如我们设置Buffer Pool的个数为16,那么那么此时chunk大小*Buffer Pool数量 = 16 * 128MB =2GB,然后Buffer Pool总大小为20GB,此时Buffer Pool总大小就是2GB的10倍,这就符合了规则。这时候一个Buffer的大小是多少? 计算得知为20GB / 16 = 1280MB, 那么包含多少个chunk呢?计算得知 1280MB / 128MB = 10个,也就是说这时候总共有16个Buffer Pool,每个Buffer Pool的大小为1280MB,每个Buffer Pool包含10个chunk。按照这个逻辑,你也可以调整Buffer Pool的数量,只要其为160的约数即可。


13、如何查看数据库的配置参数

当数据库启动之后,我们通过上述的命令 show engine innodb status 命令来进行查看,你会看到下面一些这样的数据。

=====================================
2020-08-02 10:51:59 0x1aa0 INNODB MONITOR OUTPUT
=====================================
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 8585216
Dictionary memory allocated 2806405
Buffer pool size   512
Free buffers       259
Database pages     245
Old database pages 0
Modified db pages  0
Pending reads      0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 13289358, created 73786, written 10619773
0.10 reads/s, 0.00 creates/s, 0.27 writes/s
Buffer pool hit rate 997 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 245, unzip_LRU len: 0
I/O sum[16]:cur[0], unzip sum[0]:cur[0]
--------------
END OF INNODB MONITOR OUTPUT
============================


数据很多,我这里值截取了和Buffer Pool有关的参数做一个说明。


(1)Total large memory allocated,表示Buffer Pool最终的总大小。

(2)Dictionary memory allocated,表示数据字典内存区大小。

(3)Buffer Pool size,表示Buffer Pool一共可以容纳多少个缓存页。

(4)Free buffers,表示free链表中现在一共有多少了空闲的缓存页是可用的。

(5)Database pages,表示lru链表中一共有多少个缓存页。

(6)Old Databse pages,表示lru链表的冷数据区域中一共有多少个缓存页。

(7)Modified db pages,表示flush链表中的缓存页数量。

(8)Pending reads,表示等待从磁盘加载到缓存页的数量。

(9)Pending writes,表示即将从lru链表和flush链表中刷回磁盘的数据数量。

(10)Pages made young和not young,表示已经从lru冷数据区域里访问之后被加载到热数据区域的缓存页的数量、以及在lru冷数据区域里1秒之内被访问而没有放进热数据区的缓存页数量。

(11)Pages read xxxx, created xxx, written xxx,xx reads/s, xx creates/s, 1xx writes/s,这里就是说已经读取、创建和写入了多少个缓存页,以及每秒钟读取、创建和写入的缓存页数量。

(12)Buffer Pool hit rate 997/1000,表示是说每1000次访问,有多少次直接命中了Buffer Pool里面的缓存。

(13) young-making rate 0 / 1000 not 0 / 1000,表示每1000次访问,有多少次访问让缓存页从冷数据区域移动到了热数据区域,以及没有移动的缓存页的数量。

(14)LRU len,表示LRU链表里面缓存页的总数量。

(15)unzip_LRU len,表示LRU链表里面进行了压缩还没有解压的缓存页的数量。

(16)I/O sum,表示最近50秒读取磁盘页的数量。

(17)I/O cur,表示正在读取磁盘页的数量。

14.总结

我们再来做一点总结,就是说你的数据库在生产环境运行的时候,你必须根据机器的内存设置合理的buffer pool的大小,然后设置buffer pool的数量,这样的话,可以尽可能的保证你的数据库的高性能和高并发能力。


然后在线上运行的时候,buffer pool是有多个的,每个buffer pool里多个chunk但是共用一套链表数据结构,然后执行crud的时候,就会不停的加载磁盘上的数据页到缓存页里来,然后会查询和更新缓存页里的数据,同时维护一系列的链表结构。


然后后台线程定时根据lru链表和flush链表,去把一批缓存页刷入磁盘释放掉这些缓存页,同时更新free链表。


如果执行crud的时候发现缓存页都满了,没法加载自己需要的数据页进缓存,此时就会把lru链表冷数据区域的缓存页刷入磁盘,然后加载自己需要的数据页进来。


整个buffer pool的结构设计以及工作原理,就是上面我们总结的这套东西了,大家只要理解了这个,首先你对MySQL执行crud的时候,是如何在内存里查询和更新数据的,你就彻底明白了。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
7月前
|
关系型数据库 MySQL 数据库
MySQL 的 change buffer 是什么?
MySQL 的 change buffer 是什么?
|
2月前
|
缓存 关系型数据库 MySQL
MySQL并发支撑底层Buffer Pool机制详解
【10月更文挑战第18天】在数据库系统中,磁盘IO操作是性能瓶颈之一。为了提高数据访问速度,减少磁盘IO,MySQL引入了缓存机制。其中,Buffer Pool是InnoDB存储引擎中用于缓存磁盘上的数据页和索引页的内存区域。通过缓存频繁访问的数据和索引,Buffer Pool能够显著提高数据库的读写性能。
110 2
|
3月前
|
存储 缓存 关系型数据库
深度解密 MySQL 的 Buffer Pool
深度解密 MySQL 的 Buffer Pool
41 0
|
5月前
|
SQL 缓存 关系型数据库
(十二)MySQL之内存篇:深入探寻数据库内存与Buffer Pool的奥妙!
MySQL是基于磁盘工作的,这句几乎刻在了每个后端程序员DNA里,但它真的对吗?其实答案并不能盖棺定论,你可以说MySQL是基于磁盘实现的,这点我十分认同,但要说MySQL是基于磁盘工作,这点我则抱否定的态度,至于为什么呢?这跟咱们本章的主角:Buffer Pool有关,Buffer Pool是什么?还记得咱们在《MySQL架构篇》中聊到的缓存和缓冲区么,其中所提到的写入缓冲区就位于Buffer Pool中。
395 1
|
6月前
|
存储 关系型数据库 MySQL
MySQL Change Buffer 深入解析:概念、原理及使用
MySQL Change Buffer 深入解析:概念、原理及使用
MySQL Change Buffer 深入解析:概念、原理及使用
|
6月前
|
缓存 关系型数据库 MySQL
MySQL Buffer Pool 解析:原理、组成及作用
MySQL Buffer Pool 解析:原理、组成及作用
|
6月前
|
缓存 关系型数据库 MySQL
MySQL数据库——InnoDB引擎-架构-内存结构(Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer)
MySQL数据库——InnoDB引擎-架构-内存结构(Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer)
101 3
|
6月前
|
存储 关系型数据库 MySQL
MySQL Doublewrite Buffer(双写缓冲区)深入解析:原理及作用
MySQL Doublewrite Buffer(双写缓冲区)深入解析:原理及作用
|
7月前
|
关系型数据库 MySQL 数据库
MySQL 的 change buffer 是什么?
MySQL 的 change buffer 是什么?
|
7月前
|
SQL 缓存 关系型数据库
MySQL(三)SQL优化、Buffer pool、Change buffer
MySQL(三)SQL优化、Buffer pool、Change buffer
107 0