Buffer Pool简介
Buffer Pool是MYSQL数据库中的一个重要的内存缓存组件,介于外部系统和存储引擎之间的一个缓存区,针数据库的增删改查这些操作都是针对这个内存数据结构中的缓存数据执行的,在操作数据之前,将数据从磁盘加载到Buffer Pool中,操作完成之后异步刷盘、写undolog、binlog、redolog等一些列操作,避免每次访问都进行磁盘IO影响性能。

Buffer Pool默认大小为128M,可以自行调整:
[server]innodb_buffer_pool_size=8589934592
上述配置就给Buffer Pool分配了8GB内存大小。
Buffer Pool数据存储结构
磁盘上的数据是多行放在一起组成一个一个的数据页进行存放的,每个数据页大小是16KB,在Buffer Pool中存放一个一个的数据页,通常也叫作缓存页,大小也是16KB。
磁盘中的数据页会被加载到Buffer Pool中,除了缓存页用于存储数据之外,Buffer Pool还有一个区域叫描述数据,与数据页一一对应,记录的是缓存页的元数据信息,包括数据页所属表空间、数据页编号、缓存页在Buffer Pool中的地址等等。
描述数据大概占缓存页大小的5%,大概是800个字节左右。
Buffer Pool如何管理以及淘汰内部的数据
Buffer Pool初始化
在数据库启动的时候,就会根据配置的Buffer Pool区域大小向操作系统申请内存,申请完后就会按照默认缓存页的16KB和描述数据800字节的大小将Buffer Pool划分为一个一个的缓存页和对应的描述数据,只是此时缓存页和描述数据都是空的,只有当对数据进行操作、查询的时候,才会将数据从磁盘加载到Buffer Pool中来。
为了加载时明确知道Buffer Pool中哪些缓存页是空闲的,MYSQL设计了一个free链表用于存储空闲缓存页,这是一个双向链表数据结构,在这个free链表中,每个节点就是一个空闲的缓存页的描述数据地址。
在数据库初始化Buffer Pool时,所有初始化好的空闲缓存页对应的描述数据都会放入free链表中,每个节点都会双向链接自己前后节点,组成一个双向链表。
除此之外,这个free链表中还有一个基础节点,分别指向链表的头节点和尾节点,并且存储着这个链表中有描述数据节点数量,也就是当前Buffer Pool中有空闲的数据页数量。
加载磁盘数据到Buffer Pool中
当需要加载数据到Buffer Pool中时,会首先从free链表中获取一个描述数据块,从而找到对应的空闲缓存页。
然后就可以将磁盘上的数据页读取到对应的缓存页中,同时把相关的描述数据写入缓存页对应的描述数据块中。
写完之后,将该描述数据节点从free链表上移除就可以了。
从Buffer Pool中查询数据
客户端对数据进行操作及查询时,首先会从Buffer Pool缓存区查询数据是否存在,那怎么知道这个数据页有没有在Buffer Pool中呢?
MYSQL中还有一个哈希表数据结构,用表空间号+数据页号作为key,缓存页的地址作为value。
当需要操作数据页时,首先从哈希表中根据”表空间号+数据页号”作为key进行查询,如果查询不为空的话证明数据页已经被缓存了;如果查询为空,则从磁盘进行加载,并将数据写入该哈希表,下次再使用这个数据页,就可直接从哈希表中读取。
更新Buffer Pool中的数据
当我们对数据进行更新操作时,由于是直接操作Buffer Pool缓存中的数据,势必会导致和磁盘文件中的数据页不一致,这些不一致的数据页就叫脏页。
脏页是需要刷盘的,那刷盘的时候怎么知道哪些数据页需要刷,哪些不需要刷呢?因为不可能每个数据页都刷一遍,这样效率太低了。
为了解决这个问题,MYSQL引入了另一个链表来记录更新过的脏页数据,叫flush链表,这个链表本质也是被修改过的数据页对应的描述数据块组成的一个双向链表。
flush链表的结构和free链表结构几乎一样,如下图所示:

当一个数据页被修改过后,就会将该数据页对应的描述数据块加入flush链表。
并且同free链表一样,有个基础节点,记录了flush链表节点数量,并指向头结点和尾节点。
其他的数据页被修改后,也是类似原理加入flush链表。
通过这个flush链表,就能记录目前哪些缓存页是脏页,刷盘的时候后台线程只需要处理这个链表上面记录的数据页即可,刷盘结束后,将节点从链表上抹去。
基于LRU算法淘汰Buffer Pool内部缓存页
由于不停的加载数据页到Buffer Pool中,Buffer Pool内存空间有限,迟早会出现Buffer Pool中没有空闲缓存页的情况,此时如果还需要加载数据进来,就必须有一个数据淘汰策略来淘汰一些缓存页。
淘汰缓存页的意思其实就是将一个缓存页被修改过的数据刷到磁盘的数据页中去,然后这个缓存页就可以清空了,重新变成一个空闲缓存页。
那应该淘汰哪些缓存页的数据呢?
这个需要引入一个缓存命中率的概念。
假如有两个缓存页,一个缓存页中的数据经常会被更新和查询,比如100次请求,有30次都是查询这个缓存页中的数据,此时可以说这种情况下的缓存命中率很高。
如果另一个缓存页的数据在100次请求,只被修改和查询1次,那此时就说这种情况下缓存命中率很低,因为这种情况下,大部分请求都还需要走磁盘查询。
因此,针对这两种情况,应该淘汰那个缓存页?
当然是第二个缓存页了,因为这个缓存页中的数据很少被访问,却仍然占着一个缓存页空间,这不是浪费么?
那怎么知道哪些缓存页经常被访问?哪些很少被访问?
此时需要引入一个新的LRU链表来记录,LRU就是Least Recently Used最近最少使用的意思。

LRU工作流程:
- 从磁盘加载数据页到缓存页时,将缓存页的描述数据块放到LRU链表的头部
- 如果某个缓存页的描述数据在尾部节点,只要后续对该缓存页进行了查询或者更新,都会将这个缓存页挪到LRU链表头部,即:最新访问的缓存页一定在LRU头部
- 当Buffer Pool中没有空闲缓存页了,就直接从LRU链表找到最尾部的缓存页进行刷盘即可,它一定是最近最少被访问的缓存页。
MYSQL中使用普通LRU算法的缺点:
- 预读机制导致相邻的数据页也被加载到缓存页,并且放到了LRU头部位置
预读机制是指当从磁盘加载一个数据页时,可能会连带着把这个数据页相邻的其他数据页也加载到缓存中去。
此时相邻的缓存页会占据LRU头部,然而可能后续几乎不会访问这些缓存页,之前频繁被访问的缓存页被挤到尾部被刷盘清空,这是很不合理的。
哪些情况下会触发MYSQL的预读机制呢?- 参数
innodb_read_ahead_threshold,默认值是56,这个参数的意思是如果顺序访问了一个区间的数据页,访问的数据页数量超过了这个值,就会触发预读机制,将下一个相邻区间中所有数据页都加载到缓存中 - 参数
innodb_random_read_ahead,意思是如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,将这个区里的其他数据页都加载到缓存里去。不过这个参数值默认是OFF,也就是这个规则是关闭的。
- 参数
- 全表扫描导致频繁被访问的缓存页被淘汰
比如select * from users ;这个SQL会将这个表所有的数据页都加载到缓存页中,此时LRU头部的缓存页可能就是刚刚全表扫描加载进来的缓存页,但可能后续几乎不会用到这个表里的数据,这样会将之前频繁被访问的缓存页挤到LRU尾部,这样显然不合理。
MYSQL对LRU算法进行的优化:
- 冷热数据分离
上面说的两个问题,其实不都是因为所有缓存页都在同一个LRU链表里面才会导致被加载一次就进入LRU头部的么?
所以可以将LRU链表分为两个,一个存放冷数据,一个存放热数据,MYSQL中设计的冷热数据比例是由innodb_old_blocks_pct这个参数控制的,默认是37,也就是冷数据占比37%,如图所示:
- 数据页第一次被加载到缓存页的时候,缓存页会被放到冷数据区域的头节点
- 如果这个数据页在1S后又被访问到,才被认为后续可能会经常访问这个缓存页,就会将这个缓存页挪到热数据区域头部节点去。这个时间可以通过
innodb_old_blocks)time参数进行控制,默认为1000,单位毫秒。
- 针对热数据区域进行优化
在热数据区域的缓存页,是不是只要被访问一次,就需要立马移动到头部?
如果这样的话会导致链表中的指针频繁移动,影响性能,因此MYSQL在冷热数据分离的基础上又进行了一步优化:
只有在热数据区域后3/4部分的缓存页被访问才会移动到头部去,如果是前1/4部分的缓存页,则不需移动,因为在热数据区域的头部的缓存页被淘汰的几率很小。
这个优化可以尽可能减少链表中的节点移动,从而提升性能。
Buffer Pool优化建议
1. 调整Buffer Pool内存大小
参数:innodb_buffer_pool_size,内存缓冲区大小,在内存允许的情况下,建议调大该参数值,越多的数据和索引放入缓冲区,查询性能越好。
如何设置生产环境数据库的Buffer Pool的合理内存大小,保证数据库的高性能和高并发能力?
建议调整为机器内存大小的50%~60%。
为什么不设大点?因为需要考虑操作系统占用的内存以及其他应用占用的内存、以及MYSQL中其他数据结构占用的内存。
2. 划分多个Buffer Pool区域提升数据库并发性能
在多线程并发访问同一个Buffer Pool时,他们都是访问内存中共享的数据结构,如缓存页、free链表、flush链表的等,此时必然要加锁,导致很多线程必然要串行排队,所以说是在内存中进行排队,延迟很低,但是也是有进一步优化提升性能的空间。
那如何优化呢?
给MYSQL设置多个Buffer Pool。
MYSQL默认规则是,如果Buffer Pool内存空间小于1GB,最多只分配一个Buffer Pool,如果内存空间较大,则可以设置多个Buffer Pool,配置如下:
[server]innodb_buffer_pool_size=8589934592innodb_buffer_pool_instances=4
上面的配置给Buffer Pool分配了8GB内存,设置了4个Buffer Pool,每个Buffer Pool大小就是2GB。
这样,多线程并发访问的时候就可以将压力分摊开来,多个线程可以在不同的Buffer Pool中进行加锁和执行操作,从而成倍提升多线程并发访问能力。(有点类似于ConcurrrentHashMap的分段加锁机制)
