Buffer Pool 是什么?从字面上看是缓存池的意思,没错,它其实也就是缓存池的意思。它是 MySQL 当中至关重要的一个组件,可以这么说,MySQL的所有的增删改的操作都是在 Buffer Pool 中执行的。
但是数据不是在磁盘中的吗?怎么会和缓存池又有什么关系呢?那是因为如果 MySQL的操作都在磁盘中进行,那很显然效率是很低的,效率为什么低?因为数据库要从磁盘中拿数据啊,那肯定就需要IO啊,并且数据库并不知道它将要查找的数据是磁盘的哪个位置,所以这就需要进行随机IO,那这个性能简直就别玩了。所以 MySQL对数据的操作都是在内存中进行的,也就是在 Buffer Pool 这个内存组件中。
实际上他就好比是 Redis,因为 Redis 是一个内存是数据库,他的操作就都是在内存中进行的,并且会有一定的策略将其持久化到磁盘中。那 Buffer Pool 的内存结构具体是什么样子的,那么多的增删改操作难道数据要一直在内存中吗?既然说类似 redis 缓存,那是不是也像 redis 一样也有一定的淘汰策略呢?
本篇文章,会详细的介绍 Buffer Pool 的内存结构,让大家彻底明白这里面的每一步执行流程。我们先看一下 MySQL从加载磁盘文件到完成提交一个事务的整个流程。我们先来看一个总体的流程图,从数据在磁盘中被加载到缓存池中,然后经过一些列的操作最终又被刷入到磁盘的一个过程,都经历了哪些事情,这个图不明白没有关系,因为本文重点是 Buffer Pool 这个整体的流程就是让大家稍微有个印象。
show variables like "innodb_buffer_%"
刚刚介绍到 MySQL在执行增删改的时候数据是会被加载到 Buffer Pool 中的,既然这样数据是怎么被加载进来的,是一条一条还是说是以其他的形式呢。我们操作的数据都是以表 + 行的方式,而表 + 行仅仅是逻辑上的概念,MySQL并不会像我们一样去操作行数据,而是抽象出来一个一个的数据页概念,每个数据页的大小默认是 16KB,这些参数都是可以调整的。但是建议使用默认的就好,毕竟 MySQL能做到极致的都已经做了。每个数据页存放着多条的数据,MySQL在执行增删改首先会定位到这条数据所在数据页,然后会将数据所在的数据页加载到 Buffer Pool 中。
当数据页被加载到缓冲池中后,Buffer Pool 中也有叫缓存页的概念与其一一对应,大小同样是 16KB,但是 MySQL还为每个缓存也开辟额外的一些空间,用来描述对应的缓存页的一些信息,例如:数据页所属的表空间,数据页号,这些描述数据块的大小大概是缓存页的15%左右(约800KB)。
# 缓存页是什么时候被创建的?
当 MSql 启动的时候,就会初始化 Buffer Pool,这个时候 MySQL 会根据系统中设置的 innodb_buffer_pool_size 大小去内存中申请一块连续的内存空间,实际上在这个内存区域比配置的值稍微大一些,因为【描述数据】也是占用一定的内存空间的,当在内存区域申请完毕之后, MySql 会根据默认的缓存页的大小(16KB)和对应`缓存页*15%`大小(800B左右)的数据描述的大小,将内存区域划分为一个个的缓存页和对应的描述数据
上面是说了每个数据页会被加载到一个缓存页中,但是加载的时候 MySQL是如何知道那个缓存页有数据,那个缓存页没有数据呢?换句话说, MySQL是怎么区分哪些缓存页是空闲的状态,是可以用来存放数据页的。
为了解决这个问题, MySQL 为 Buffer Pool 设计了一个双向链表— free链表,这个 free 链表的作用就是用来保存空闲缓存页的描述块(这句话这么说其实不严谨,换句话:每个空闲缓存页的描述数据组成一个双向链表,这个链表就是free链表)。之所以说free链表的作用就是用来保存空闲缓存页的描述数据是为了先让大家明白 free 链表的作用,另外 free 链表还会有一个基础节点,他会引用该链表的头结点和尾结点,还会记录节点的个数(也就是可用的空闲的缓存页的个数)。
这个时候,他可以用下面的图片来描述:
当加载数据页到缓存池中的时候, MySQL会从 free 链表中获取一个描述数据的信息,根据描述节点的信息拿到其对应的缓存页,然后将数据页信息放到该缓存页中,同时将链表中的该描述数据的节点移除。这就是数据页被读取 Buffer Pool 中的缓存页的过程。
但 MySQL是怎么知道哪些数据页已经被缓存了,哪些没有被缓存呢。实际上数据库中还有后一个哈希表结构,他的作用是用来存储表空间号 + 数据页号作为数据页的key,缓存页对应的地址作为其value,这样数据在加载的时候就会通过哈希表中的key来确定数据页是否被缓存了。
MySql 在执行增删改的时候会一直将数据以数据页的形式加载到 Buffer Pool 的缓存页中,增删改的操作都是在内存中执行的,然后会有一个后台的线程数将脏数据刷新到磁盘中,但是后台的线程肯定是需要知道应该刷新哪些啊。
针对这个问题,MySQL设计出了 Flush 链表,他的作用就是记录被修改过的脏数据所在的缓存页对应的描述数据。如果内存中的数据和数据库和数据库中的数据不一样,那这些数据我们就称之为脏数据,脏数据之所以叫脏数据,本质上就是被缓存到缓存池中的数据被修改了,但是还没有刷新到磁盘中。
同样的这些已经被修改了的数据所在的缓存页的描述数据会被维护到 Flush 中(其实结构和 free 链表是一样的),所以 Flush 中维护的是一些脏数据数据描述(准确地说是脏数据的所在的缓存页的数据描述)
另外,当某个脏数据页页被刷新到磁盘后,其空间就腾出来了,然后又会跑到 Free 链表中了。
mysql是基于冷热数据分离的LRU链表,所谓的冷热分离,就是将 LRU 链表分成两部分,一部分是经常被使用到的热数据,另一部分是被加载进来但是很少使用的冷数据。通过参数innodb_old_blocks_pct 参数控制的,默认为37,也就是 37% 。用图表示大致如下:
数据在从磁盘被加载到缓存池的时候,首先是会被放在冷数据区的头部,然后在一定时间之后,如果再次访问了这个数据,那么这个数据所在的缓存页对应描述数据就会被放转移到热数据区链表的头部。
那为什么说是在一定的时间之后呢,假设某条数据刚被加载到缓存池中,然后紧接着又被访问了一次,这个时候假设就将其转移到热数据区链表的头部,但是以后就再也不会被使用了,这样子是不是就还是会存在之前的问题呢?
所以 MySQL通过innodb_old_blocks_time来设置数据被加载到缓存池后的多少时间之后再次被访问,才会将该数据转移到热数据区链表的头部,该参数默认是1000单位为:毫秒,也就是1秒之后,如果该数据又被访问了,那么这个时候才会将该数据从 LRU 链表的冷数据区转移到热数据区。
# free链表 用来存放空闲的缓存页的描述数据,如果某个缓存页被使用了,那么该缓存页对应的描述数据就会被从free链表中移除 # flush链表 被修改的脏数据都记录在 Flush 中,同时会有一个后台线程会不定时的将 Flush 中记录的描述数据对应的缓存页刷新到磁盘中,如果某个缓存页被刷新到磁盘中了,那么该缓存页对应的描述数据会从 Flush 中移除,同时也会从LRU链表中移除(因为该数据已经不在 Buffer Pool 中了,已经被刷入到磁盘,所以就也没必要记录在 LRU 链表中了),同时还会将该缓存页的描述数据添加到free链表中,因为该缓存页变得空闲了。 # LRU链表 数据页被加载到 Buffer Pool 中的对应的缓存页后,同时会将缓存页对应的描述数据放到 LRU 链表的冷数据的头部,当在一定时间过后,冷数据区的数据被再次访问了,就会将其转移到热数据区链表的头部,如果被访问的数据就在热数据区,那么如果是在前25%就不会移动,如果在后75%仍然会将其转移到热数据区链表的头部
后台线程将冷数据区的尾节点的描述数据对应的缓存页刷入磁盘文件中
我们平时的系统绝对不可能每次只有一个请求来访问的,说白了就是如果多个请求同时来执行增删改,那他们会并行的去操作 Buffer Pool 中的各种链表吗?如果是并行的会不会有什么问题。
实际上 MySQL在处理这个问题的时候考虑得非常简单,就是: Buffer Pool 一次只能允许一个线程来操作,一次只有一个线程来执行这一系列的操作,因为MySQL 为了保证数据的一致性,操作的时候必须缓存池加锁,一次只能有一个线程获取到锁。
这个时候,大家这时候肯定满脑子问号。串行那还谈什么效率?大家别忘记了,这一系列的操作都是在内存中操作的,实际上这是一个瞬时的过程,在内存中的操作基本是几毫秒的甚至微妙级别的事情。
但是话又说回来,串行执行再怎么快也是串行,虽然不是性能瓶颈,这还有更好的优化办法吗?那肯定的 MySQL早就设计好了这些规则。那就是 Buffer Pool 是可以有多个的,可以通过 MySQL的配置文件来配置,参数分别是:
# Buffer Pool 的总大小
innodb_buffer_pool_size=9663676416
# Buffer Pool 的实例数(个数)
innodb_buffer_pool_instance=3
一般在生产环境中,在硬件不紧张的情况下,建议使用此策略。这个时候大家是不是又会有一个疑问(如果没有那说明你没认真思考哦),大家应该有这样的疑问:
# 问:多个 Buffer Pool 所带来的问题思考
在多个线程访问不同的 Buffer Pool 那不同的线程加载的数据必然是在不同的 Buffer Pool 中,假设 A 线程加载数据页A到 Buffer Pool A 中,B 线程加载数据页B到 Buffer Pool B 中,然后两个都执行完了,这个时候 C 线程来了,他到达的是 Buffer Pool B中,但是 C 要访问的数据是在 Buffer Pool A中的数据页上了,这个时候 C 还会去加载数据页A吗?,这种情况会发生吗?在不同的 Buffer Pool 缓存中会去缓存相同的数据页吗?
# 答:多个 Buffer Pool 所带来的问题解答
这种情况很显然不会发生,既然不会发生,那 MySql 是如何解决这种问题的?其实前面已经提到过了,那就是 数据页缓存哈希表(看下图),里面存放的是表空间号+数据页号 = 缓存页地址,所以 MySQL 在加载数据所在的数据页的时候根据这一系列的映射关系判断数据页是否被加载,被加载到了那个缓存页中,所以 MySQL 能够精确的确定某个数据页是否被加载,被加载的到了哪个缓存页,绝不可能出现重复加载的情况。
MySQL采用 chunk机制。
# 什么是chunk机制 chunk是 MySQL 设计的一种机制,这种机制的原理是将 Buffer Pool 拆分一个一个大小相等的 chunk 块,每个 chunk 默认大小为 128M(可以通过参数innodb_buffer_pool_chunk_size 来调整大小),也就是说 Buffer Pool 是由一个个的chunk组成的 假设 Buffer Pool 大小是2GB,而一个chunk大小默认是128M,也就是说一个2GB大小的 Buffer Pool 里面由16个 chunk 组成,每个chunk中有自己的缓存页和描述数据,而 free 链表、flush 链表和 lru 链表是共享的
mysql动态调整大小,其实就是基于申请chunk来进行的。
原文链接:https://www.toutiao.com/i6923175484478079500/