myql中的Bufffer pool

前言

实际上我们对数据库执行增删改操作的时候,实际上主要都是针对内存里的Buffer Pool中的数据进行的,也就是你实际上主要是对数据库的内存里的数据结构进行了增删改。同时配合了后续的redo log、刷磁盘等机制和操作。所以Buffer Pool就是数据库的一个内存组件,里面缓存了磁盘上的真实数据,然后我们的Java系统对数据库执行的增删改操作,其实主要就是对这个内存数据结构中的缓存数据执行的
在这里插入图片描述

Buffer Pool:数据结构

磁盘数据结构:数据页

在这里插入图片描述
实际上假设我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接给加载到Buffer Pool里去也就是说,Buffer Pool中存放的是一个一个的数据页,如下图。
在这里插入图片描述

缓冲池数据结构:数据页(缓存页)

实际上默认情况下,磁盘中存放的数据页的大小是16KB,也就是说,一页数据包含了16KB的内容。
而Buffer Pool中存放的一个一个的数据页,我们通常叫做缓存页,因为毕竟Buffer Pool是一个缓冲池,里面的数据都是从磁盘缓存到内存去的。而Buffer Pool中默认情况下,一个缓存页的大小和磁盘上的一个数据页的大小是一一对应起来的,都是16KB
在这里插入图片描述

缓存页对应的描述信息

对于每个缓存页,他实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓存页的。
比如包含如下的一些东西:这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址以及别的一些杂七杂八的东西。每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据,在Buffer Pool中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面。
在这里插入图片描述
Buffer Pool中的描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大概是800个字节左右的大小,然后假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130多MB的样子,因为他里面还要存放每个缓存页的描述数据。

Buffer Pool:初始化

数据库只要一启动,就会按照你设置的Buffer Pool大小,稍微再加大一点,去找操作系统申请一块内存区域,作为Buffer Pool的内存区域。然后当内存区域申请完毕之后,数据库就会按照默认的缓存页的16KB的大小以及对应的800个字节左右的描述数据的大小,在Buffer Pool中划分出来一个一个的缓存页和一个一个的他们对应的描述数据。然后当数据库把Buffer Pool划分完毕之后,看起来就是之前我们看到的那张图了,如下图所示
在这里插入图片描述

只不过这个时候,Buffer Pool中的一个一个的缓存页都是空的,里面什么都没有,要等数据库运行起来之后,当我们要对数据执行增删改查的操作的时候,才会把数据对应的页从磁盘文件里读取出来,放入Buffer Pool中的缓存页中。

Buffer Pool中的链表

Buffer Pool:free链表

当你的数据库运行起来之后,你肯定会不停的执行增删改查的操作,此时就需要不停的从磁盘上读取一个一个的数据页放入Buffer Pool中的对应的缓存页里去,把数据缓存起来,那么以后就可以对这个数据在内存里执行增删改查了。
但是此时在从磁盘上读取数据页放入Buffer Pool中的缓存页的时候,必然涉及到一个问题,那就是哪些缓存页是空闲的?因为默认情况下磁盘上的数据页和缓存页是一 一对应起来的,都是16KB,一个数据页对应一个缓存页。所以我们必须要知道Buffer Pool中哪些缓存页是空闲的状态。

free链表,他是一个双向链表数据结构,这个free链表里,每个节点就是一个空闲的缓存页的描述数据块的地址,也就是说,只要你一个缓存页是空闲的,那么他的描述数据块就会被放入这个free链表中
数据库启动的时候,可能所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有缓存页的描述数据块,都会被放入这个free链表中
在这里插入图片描述
free链表里面就是各个缓存页的描述数据块,只要缓存页是空闲的,那么他们对应的描述数据块就会加入到这个free链表中,每个节点都会双向链接自己的前后节点,组成一个双向链表。
free链表,他本身其实就是由Buffer Pool里的描述数据块组成的,你可以认为是每个描述数据块里都有两个指针,一个是free_pre,一个是free_next,分别指向自己的上一个free链表的节点。

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

那么磁盘数据页如何读取到缓冲池数据页的?

首先,我们需要从free链表里获取一个描述数据块,然后就可以对应的获取到这个描述数据块对应的空闲缓存页,我们看下图所示:
在这里插入图片描述
接着我们就可以把磁盘上的数据页读取到对应的缓存页里去,同时把相关的一些描述数据写入缓存页的描述数据块里去,比如这个数据页所属的表空间之类的信息,最后把那个描述数据块从free链表里去除就可以了,如下图所示:
在这里插入图片描述
我们在执行增删改查的时候,肯定是先看看这个数据页有没有被缓存,如果没被缓存就走上面的逻辑,从free链表中找到一个空闲的缓存页,从磁盘上读取数据页写入缓存页,写入描述数据,从free链表中移除这个描述数据块。但是如果数据页已经被缓存了,那么就会直接使用了。

缓存页哈希表

所以其实数据库还会有一个哈希表数据结构,他会用表空间号+数据页号,作为一个key,然后缓存页的地址作为value。读取一个数据页到缓存之后,都会在这个哈希表中写入一个key-value对,key就是表空间号+数据页号,value就是缓存页的地址,那么下次如果你再使用这个数据页,就可以从哈希表里直接读取出来他已经被放入一个缓存页了。
在这里插入图片描述
在这里插入图片描述

Buffer Pool:flush链表

flush链表本质也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表。凡是被修改过的缓存页,都会把他的描述数据块加入到flush链表中去,flush的意思就是这些都是脏页,后续都是要flush刷新到磁盘上去的,所以flush链表的结构如下图所示,跟free链表几乎是一样的。
在这里插入图片描述

Buffer Pool:LRU链表

LRU就是Least Recently Used,最近最少使用的意思。通过这个LRU链表,我们可以知道哪些缓存页是最近最少被使用的,那么当你缓存页需要腾出来一个刷入磁盘的时候,不就可以选择那个LRU链表中最近最少被使用的缓存页了么?
假设我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链表头部去,那么只要有数据的缓存页,他都会在LRU里了,而且最近被加载数据的缓存页,都会放到LRU链表的头部去。

只要我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链表头部去,那么只要有数据的缓存页,他都会在LRU里了,而且最近被加载数据的缓存页,都会放到LRU链表的头部去。因此在进行淘汰缓存页的时候,直接在LRU链表的尾部找到一个缓存页,然后你就把LRU链表尾部的那个缓存页刷入磁盘中,然后把你需要的磁盘数据页加载到腾出来的空闲缓存页中就可以了
在这里插入图片描述

全盘扫描

SELECT * FROM USERS,此时他没加任何一个where条件,会导致他直接一下子把这个表里所有的数据页,都从磁盘加载到Buffer Pool里去,大量的数据页会把空闲缓存页用完。
最终LRU链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把热点数据页给淘汰了

冷热分离LRU

真正的LRU链表,会被拆分为两个部分,一部分是热数据,一部分是冷数据,这个冷热数据的比例是由innodb_old_blocks_pct参数控制的,他默认是37,也就是说冷数据占比37%。第一次被加载了数据的缓存页,都会不停的移动到冷数据区域的链表头部。
在这里插入图片描述
MySQL设定了一个规则,他设计了一个innodb_old_blocks_time参数,默认值1000,也就是1000毫秒。假设你加载了一个数据页到缓存去,然后过了1s之后你还访问了这个缓存页,说明你后续很可能会经常要访问它,这个时间限制就是1s,因此只有1s后你访问了这个缓存页,他才会给你把缓存页放到热数据区域的链表头部去
在这里插入图片描述

为什么1s后再次访问才移动至热区域
因为表的批量加载,正常来讲只加载一次,如果是其中一页查找的速度是很快的,所以全表扫描的基本上不会出现第二次查询和第一次查询时间间隔超出1s,这样就可以避免全表扫描数据进入热区域。

热数据LRU移动

接着我们来看看LRU链表的热数据区域的一个性能优化的点,就是说,在热数据区域中,如果你访问了一个缓存页,是不是应该要把他立马移动到热数据区域的链表头部去?如果这样的话,是不是这么频繁的进行移动是不是性能也并不是太好?

当并发量大的时候,因为要加锁,会存在锁竞争,每次移动显然效率就会下降。因此 MySQL 针对这一点又做了优化,如果一个缓存页处于热数据区域,且在热数据区域的前 1/4 区域(注意是热数据区域的 1/4,不是整个链表的 1/4),那么当访问这个缓存页的时候,就不用把它移动到热数据区域的头部;如果缓存页处于热数据的后 3/4 区域,那么当访问这个缓存页的时候,会把它移动到热数据区域的头部

举个例子,假设热数据区域的链表里有100个缓存页,那么排在前面的25个缓存页,他即使被访问了,也不会移动到链表头部去的。但是对于排在后面的75个缓存页,他只要被访问,就会移动到链表头部去。这样的话,他就可以尽可能的减少链表中的节点移动了

冷数据LRU刷盘

首先第一个时机,并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台线程,他会运行一个定时任务,这个定时任务每隔一段时间就会把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘里去,清空这几个缓存页,把他们加入回free链表去!所以实际上在缓存页没用完的时候,可能就会清空一些缓存页了,我们看下面的图示
在这里插入图片描述

Buffer pool设置

多个Buffer Pool优化并发能力

MySQL同时接收到了多个请求,他自然会用多个线程来处理这多个请求,每个线程会负责处理一个请求。
在这里插入图片描述
现在多个线程来并发的访问这个Buffer Pool了,此时他们都是在访问内存里的一些共享的数据结构,比如说缓存页、各种链表之类的,那么此时是不是必然要进行加锁?对,多线程并发访问一个Buffer Pool,必然是要加锁的,然后让一个线程先完成一系列的操作,比如说加载数据页到缓存页,更新free链表,更新lru链表,然后释放锁,接着下一个线程再执行一系列的操作。

大部分情况下,每个线程都是查询或者更新缓存页里的数据,这个操作是发生在内存里的,基本都是微秒级的,很快很快,包括更新free、flush、lru这些链表,他因为都是基于链表进行一些指针操作,性能也是极高的。

因此我们可以以给MySQL设置多个Buffer Pool来优化他的并发能力。
一般来说,MySQL默认的规则是,如果你给Buffer Pool分配的内存小于1GB,那么最多就只会给你一个BufferPool。
但是如果你的机器内存很大,那么你必然会给Buffer Pool分配较大的内存,比如给他个8G内存,那么此时你是同时可以设置多个Buffer Pool的,比如说下面的MySQL服务器端的配置。

[server]
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默认情况下是128MB,还是有一点偏小了,我们实际生产环境下完全可以对Buffer Pool进行调整

比如我们的数据库如果是16核32G的机器,那么你就可以给Buffer Pool分配个2GB的内存,使用下面的配置就可以了。

nnodb_buffer_pool_size = 2147483648

我们可以先查看下我们innodb_buffer_pool_size的大小,执行如下SQL,可以发现为134217728【134217728 /1024 / 1024 =128M】

show global variables like ‘innodb_buffer_pool_size’;

设置大小

SET GLOBAL innodb_buffer_pool_size= 32423423

Buffer pool总内存应该设置多少?

建议一个比较合理的、健康的比例,是给buffer pool设置你的机器内存的50%~60%左右比如你有32GB的机器,那么给buffer设置个20GB的内存,剩下的留给OS和其他人来用,这样比较合理一些。假设你的机器是128GB的内存,那么buffer pool可以设置个80GB左右,大概就是这样的一个规则。

Buffer pool应该设置多少个?

接着确定了buffer pool的总大小之后,就得考虑一下设置多少个buffer pool,以及chunk的大小了
此时要记住,有一个很关键的公式就是:
buffer pool总大小 = chunk大小 x chunk的个数 x buffer pool数量

总结

首先,缓冲池申请的内存空间一定是页大小(默认16KB)的倍数,换句话说,虽然缓冲池是一块很大的内存区域,然而在使用时是根据固定的页大小进行管理的。如图所示∶
在这里插入图片描述
缓冲池有一个 free 链表,其中保存着未被使用的内存页空间。当 free 链表中的页都已分配完毕,当再要申请空间时,则需要根据LRU(Latest Recent Used 最近最少使用)算法淘汰已经使用的页。

通常来说,数据库中的缓冲池都是通过 LRU(Latest Recent Used 最近最少使用)算法来进行管理的。即最频繁使用的页在 LRU链表的前端,而最少使用的页在 LRU链表的尾端。当缓冲池不能存放新读取到的页时,将首先从LRU 链表中释放尾端的页。
在InnoDB存储引擎中,其同样使用 LRU 算法对缓冲池进行管理。稍有不同的是 InnoDB存储引擎对传统的LRU算法做了一些优化。在InnoDB的存储引擎中,LRU链表中还加入了midpoint 位置,新读取到的页,虽然是最新访问的页,但并不是直接放入到LRU 链表的首部,而是放入到LRU链表的 midpoint 位置。这个算法在 InnoDB存储引擎下称为 midpoint insertion strategy。默认配置下,该位置在 LRU链表长度的 3/8处,如图所示。
在这里插入图片描述
那为什么不采用朴素的 LRU 算法,直接将读取的页放入到LRU链表的首部呢?这是因为若直接将读取到的页放入到LRU的首部,那么某些 SOL 操作可能会使得缓冲池中的页从LRU 链表中被刷新出,从而影响缓冲池的效率。常见的这类操作为索引或者数据的扫描操作。这类操作需要访问表中的许多页,甚至是全部的页,而这些页通常来说仅在这次查询操作中需要,并不是活跃的热点数据。如果页被放入 LRU 链表的首部,那么非常可能将所需要的热点数据页从LRU链表中移除,而当下一次需要读取该页时,InnoDB存储引擎需要再次访问磁盘,从而导致数据库性能的下降。

缓冲池中的页不仅需要被读取,还需要进行修改操作。修改的页肯定发生在 LRU链表中,当LRU链表中的页被修改后,则称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页数据产生了不一致。这时数据库会通过 checkpoint 机制将脏页刷新回磁盘。而 flush 链表中的页即为脏页。需要注意的是,脏页既存在于LRU链表中,也存在于flush链表中。LRU链表用于管理缓冲池中页的可用性,flush 链表则用干管理将页刷新回磁盘,两者互不影响。显示了 free 链表、LRU链表、flush 链表之间的关系∶
在这里插入图片描述

文章来源:https://www.saoniuhuo.com/article/detail-484156.html
总结:https://blog.csdn.net/lijuncheng963375877/article/details/124011204
其他文章推荐:https://zhuanlan.zhihu.com/p/408119486
视频链接:https://www.bilibili.com/video/BV1Pv411h7Ep

猜你喜欢

转载自blog.csdn.net/yzx3105/article/details/130736132