一次性将MySQL中的Buffer Pool搞懂

前期回顾

在前面我们学习了Redo Log日志,它的作用说白了,就是保存你的操作,也许你会有这样的疑惑,为什么就Redo Log有一个Buffer在内存中。

mysql general_log 过大 mysql logbuffer_lru

Buffer Pol的地位

众所周知,硬盘的速度是非常慢的,而CPU的速度是非常快的,两者的速度天差地别,因此需要固态硬盘作为缓存来缓解两者速度差距过大而出现CPU等待硬盘的情况。所以,对于CPU来说,当它想要对某些数据进行操作的时候,会先把这些数据缓存到内存中,然后在内存中对其进行操作,这样可以提高IO效率,因为不需要在每次读写的时候都去访问磁盘,而Buffer Pool就是用来缓存磁盘文件中的数据的。同理,因为Redo Log是用来记录操作的,所以对它操作的频率基本上和对数据操作的频率一样,如果没有Redo Log Buffer,结果可想而知,因此伴随着Buffer Pool,在内存中也会有一个Redo Log Buffer

通过上面的讲述,相信你能感觉到Buffer Pool的重要性了,因为对数据的操作都会在Buffer Pool中进行!

Buffer Pool长什么样子?

在MySQL中,Buffer Pool本质上就是个存储数据的数据结构,它的默认总大小是128M,这个默认大小是可以通过命令 innodb-buffer-pool-size = xxxxxxx 按照需求进行修改的。

  • 那么里面的数据具体是怎么存储的呢?
    其实数据都存储在一个个的 数据页
  • 那每个数据页是怎么存储数据库中的数据的呢?
    中我们都知道,数据库存储有库、表、行和字段的概念,你也许会想,那会不会一个数据页存储一个表的数据呢?很显然这不合理,因为有的表存的数据可能很多,有的表存储的数据也可能很少,这样一来数据页之间的大小可能差距很大。那会不会是一个数据页存储一行数据呢?其实也不是。在说这个之前,我想先给大家说一个对磁盘进行读写时的一个小知识点,对于磁盘文件的读写,是小单位是16k,也就是说,你每次对磁盘进行读操作时假如你想读取的数据是1k大小,但是实际上是16k。我们也知道,数据库中一行数据可能1k都没有,假如你每个数据页存一行数据,但是你每次读取的时候是16k的数据,那岂不是很不合理?因此每个数据页的大小是16k,它里面存储了多行数据
    大致就像下面这样:

mysql general_log 过大 mysql logbuffer_buffer pool_02

  • 缓存页对应的描述信息是什么?
    因为我们的增删改操作是对MySQL中的Buffer Pool进行的,为了能够让这种操作像是对MySQL一样,就需要一个专门的数据结构来描述前面提到的数据页——描述数据,它里面包含了表空间号、数据页编号以及数据页在内存中的地址等信息,也是存在MySQL中,其大小大约是缓存页的5%。所以,加入你设置的Buffer Pool大小的128M,实际可能是130M,因为除了存放数据页,还要存放描述信息
    像下面这个样子:

mysql general_log 过大 mysql logbuffer_mysql_03

数据是如何保存到缓存页中的?

目前我们知道,Buffer Pool被分成了许多个的数据页,那么,当数据从磁盘文件中加载到Buffer Pool中的时候,它是如何知道哪些数据页是恶意存放数据的呢?实际上,每个描述数据内有两个指针,分别是free_pre和free_next,有了这两个指针后,所有的空闲数据页就可以在逻辑上组成一个free双向链表,长得就像下面这个图:

mysql general_log 过大 mysql logbuffer_缓存_04

需要注意的是,Free链表中的描述数据和Buffer Pool中的描述数据其实是同一个,只不过在逻辑上他们组成了双向链表。

当有数据从磁盘文件加载到Buffer Pool中的时候,就会从Free链表中找一个描述数据块,将数据放入其对应的缓存页。

怎么知道数据是否已经被缓存了呢?

我们先来回理一下增删改操作,当需要对某个数据进行增删改操作的时候,首先会检查一下,这个数据是否已经在内存中被缓存,如果有的话,那么就直接对内存中的数据进行操作即可,如果缓存中没有,那么就会先从磁盘将数据加载到Buffer Pool中来然后在内存中对其进行操作。

那么我们如何得知我们要的数据是否在已经被缓存了呢?首先,我们要知道的是我们基于内存去找某个数据是否在内存中的时候,其实是在找他所在的数据页是否在内存中,因为每个缓存页都有对应的描述数据。我们知道对于搜索,又很多种方法,比如线性表,树,哈希表等。这里为了达到快速的搜索,采用的是哈希表这一数据结构,K是表空间号+数据页号,V是缓存页的地址,每当你需要查找某个缓存页的时候,都回去这个哈希表中去搜索。

注:这里的表空间号和数据页号都是物理层面的概念,而我们熟知的库,表,行,字段都是逻辑上的概念

mysql general_log 过大 mysql logbuffer_数据页_05

脏数据到底是怎回事?

我们知道,MySQL中的Buffer pool是由缓存页组成,然后随着我们不断的增删改,磁盘中的数据也会不断的被夹在到Buffer Pool中,因为Buffer Pool的大小是有限的,肯定是会满的,那万一放不下了该怎么办呢?不难想到的是,MySQL会将某些数据页清空,用来放后面进来的数据,那是不是随便找一个数据页进行清理呢?当然不是,这样太鲁莽了,之前也说过,增删改操作是针对于数据页中的数据,并没有立刻去操作磁盘中的数据。万一数据还没刷入磁盘突然被清理掉了,那肯定会出现这种情况:A修改了某条数据,后面在查询的时候发现竟然还是原来的样子。对于数据页中已经被修改但是还未及时同步到磁盘文件的数据,我们称之为脏数据,他所在的缓存页我们称之为脏页。

如何判断哪些是脏数据呢?

通过刚才的分析,我们知道了脏数据是必须要刷入磁盘的,目的就是使内存中的数据和磁盘中的数据保持一致。但是你想将它们刷入磁盘之前,必须要先知道哪些数据页需要被刷入磁盘,那么MySQL是怎么做的呢?其实呀方法和知道哪些缓存页是空闲的类似,就是让描述数据中有两个指针:flush_pre、flush_next,利用这两个指针在逻辑上组成一个flush链表,一旦某个数据页的数据发生改变,就会将它加入到flush链表中,然后在后台会有专门的IO线程将它们刷入磁盘

mysql general_log 过大 mysql logbuffer_lru_06

当缓存页不够用的时候该怎么办?

上面也分析过了。当缓存页不够用的时候肯定是不会选择清理脏数据页的,那到底应该清理哪些呢?通过上面的分析,我们可以判断的是,被清空的缓存页肯定是没有被修改过的数据,那是不是随机进行清理的呢?在说这个之前,我想先补充个概念——缓存命中率,它指的是在访问缓存的请求的数量在总请求数量的占比,是一个用来判断缓存达到目的的有效依据。命中率越高,从磁盘读取数据的频率就越低,性能也就越好。那么如何提高缓存命中率呢?我们都知道,生活中有一个热搜的概念,对于某些话题,在刚出现的时候讨论很激烈,之后就没多少人说了。对于数据来说,也是如此,有些数据是热点数据,而有些数据则无人问津,并且热点数据被访问的次数肯定很多,既然如此,我们肯定希望的是它能长期的在内存中,这样就会减少很多访问磁盘的操作。新的问题又来了,我们怎么知道那些数据是热点数据呢?和上面讲过的方法也很类似,也是利用双向链表进行记录,但是为了区分热点数据,使用的是LRU链表——Laest Recently Used,意思是最近最少使用。原理也很简单,每当有数据从磁盘中被加载到缓存页的时候,都会将这个缓存页加入到LRU链表的头部,并且当LRU链表中某个数据页被访问的时候,都会将他放到头部,这样一来,经常访问的都会集中在链表前面,处于链表末尾的数据就是最近最少使用的数据了。在内存不够用的时候,就会将其清空掉,用来存放刚加载进来的数据。

mysql general_log 过大 mysql logbuffer_缓存_07


今天先更新到这里,先知道Buffer Pool中各种链表的作用,之后的文章,我会去深入一下这其中可能出现的问题,以及其应对方案。


今天先更新到这里,先知道Buffer Pool中各种链表的作用,之后的文章,我会去深入一下这其中可能出现的问题,以及其应对方案。

如果文章中有问题,还望指出,共同进步。写文章不易,如果对你有帮助记得点赞哦~