mysql的Buffer Pool
一,Buffer Pool
Buffer Pool是MYSQL数据库中的一个重要的内存组件,介于外部系统和存储引擎之间的一个缓存区,针数据库的增删改查这些操作都是针对这个内存数据结构中的缓存数据执行的,在操作数据之前,都会将数据从磁盘加载到Buffer Pool中,操作完成之后异步刷盘、写undo log、binlog、redolog等一些列操作,避免每次访问都进行磁盘IO影响性能。
Buffer Pool默认大小为128M,可以自行调整:
[server]
innodb_buffer_pool_size=8589934592上述配置就给Buffer Pool分配了8GB内存大小
1.1 Buffer Pool数据存储结构
磁盘上的数据是通过多行放在一起组成一个一个的数据页进行存放的,每个数据页大小是16KB,在Buffer Pool中存放一个一个的数据页,大小也是16KB,通常也叫作缓存页。
磁盘中的数据页会被加载到Buffer Pool中,除了缓存页用于存储数据之外,Buffer Pool还有一个区域叫描述数据,分别对应一个数据页,记录的是缓存页的元数据信息,包括数据页所属表空间、数据页编号、缓存页在Buffer Pool中的地址等等,描述数据大概占缓存页大小的5%,大概是800个字节左右。
1.2 Buffer Pool初始化-free链表
在数据库启动的时候,就会根据配置的Buffer Pool区域大小向操作系统申请内存,申请完后就会按照默认缓存页的16KB和描述数据800字节的大小将Buffer Pool划分为一个一个的缓存页和对应的描述数据,只是此时缓存页和描述数据都是空的,只有当对数据进行操作、查询的时候,才会将数据从磁盘加载到Buffer Pool中来。
加载时为了明确知道Buffer Pool中哪些缓存页是空闲的,MYSQL设计了一个free链表用于存储空闲缓存页,这是一个双向链表数据结构,在这个free链表中,每个节点就是一个空闲的缓存页的描述数据地址。
在数据库初始化Buffer Pool时,所有初始化好的空闲缓存页对应的描述数据都会放入free链表中,每个节点都会双向链接自己前后节点,组成一个双向链表。
除此之外,这个free链表中还有一个基础节点,分别指向链表的头节点和尾节点,并且存储着这个链表中有描述数据节点数量,也就是当前Buffer Pool中有空闲的数据页数量.
1.3 查询Buffer Pool中数据-free链表
客户端对数据进行操作及查询时,首先会从Buffer Pool缓存区查询数据是否存在,那怎么知道这个数据页有没有在Buffer Pool中呢?
MYSQL中还有一个哈希表数据结构,用表空间号+数据页号作为key,缓存页的地址作为value,当需要操作数据页时,首先从哈希表中根据"表空间号+数据页号"作为key进行查询,如果查询不为空的话证明数据页已经被缓存了;如果查询为空,则从磁盘进行加载,并将数据写入该哈希表,下次再使用这个数据页,就可直接从哈希表中读取。
1.4 更新Buffer Pool中数据-flush链表
当我们对数据进行更新操作时,由于是直接操作Buffer Pool缓存中的数据,势必会导致和磁盘文件中的数据页不一致,这些不一致的数据页就叫脏页。
脏页是需要刷盘的,那刷盘的时候怎么知道哪些数据页需要刷,哪些不需要刷呢?因为不可能每个数据页都刷一遍,这样效率太低了。
为了解决这个问题,MYSQL引入了另一个链表来记录更新过的脏页数据,叫flush链表,这个链表本质也是被修改过的数据页对应的描述数据块组成的一个双向链表。
flush链表的结构和free链表结构几乎一样,如下图所示:
当一个数据页被修改过后,就会将该数据页对应的描述数据块加入flush链表,并且同free链表一样,有个基础节点,记录了flush链表节点数量,并指向头结点和尾节点,其他的数据页被修改后,也是类似原理加入flush链表,通过这个flush链表,就能记录目前哪些缓存页是脏页,刷盘的时候后台线程只需要处理这个链表上面记录的数据页即可,刷盘结束后,将节点从链表上抹去。
1.5 基于LRU算法淘汰Buffer Pool内部缓存页-LRU链表
由于不停的加载数据页到Buffer Pool中,Buffer Pool内存空间有限,迟早会出现Buffer Pool中没有空闲缓存页的情况,此时如果还需要加载数据进来,就必须有一个数据淘汰策略来淘汰一些缓存页。
淘汰缓存页的意思其实就是将一个缓存页被修改过的数据刷到磁盘的数据页中去,然后这个缓存页就可以清空了,重新变成一个空闲缓存页,方便记载新的数据。
1,问:那应该淘汰哪些缓存页的数据呢?
答:缓存命中率低的。
2,问:那怎么知道哪些缓存页经常被访问?哪些很少被访问?
答:引入一个新的LRU链表来记录,LRU就是Least Recently Used最近最少使用的意思,头部是缓存命中率高的,尾部存放缓存命中率低的。
LRU工作流程:
- 从磁盘加载数据页到缓存页时,将缓存页的描述数据块放到LRU链表的头部
- 如果某个缓存页的描述数据在尾部节点,只要后续对该缓存页进行了查询或者更新,都会将这个缓存页挪到LRU链表头部,即:最新访问的缓存页一定在LRU头部
- 当Buffer Pool中没有空闲缓存页了,就直接从LRU链表找到最尾部的缓存页进行刷盘即可,它一定是最近最少被访问的缓存页。
1.6 LRU算法缺点
问题一:预读机制导致相邻的数据页也被加载到缓存页,并且放到了LRU头部位置,预读机制是指当从磁盘加载一个数据页时,可能会连带着把这个数据页相邻的其他数据页也加载到缓存中去。此时相邻的缓存页会占据LRU头部,然而可能后续几乎不会访问这些缓存页,之前频繁被访问的缓存页被挤到尾部被刷盘清空,这是很不合理的。哪些情况下会触发MYSQL的预读机制呢?
参数innodb_read_ahead_threshold,默认值是56,这个参数的意思是如果顺序访问了一个区间的数据页,访问的数据页数量超过了这个值,就会触发预读机制,将下一个相邻区间中所有数据页都加载到缓存中
- 参数innodb_random_read_ahead,意思是如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,将这个区里的其他数据页都加载到缓存里去。不过这个参数值默认是OFF,也就是这个规则是关闭的。
问题二:全表扫描导致频繁被访问的缓存页被淘汰,比如
select * from users ;这个SQL会将这个表所有的数据页都加载到缓存页中,此时LRU头部的缓存页可能就是刚刚全表扫描加载进来的缓存页,但可能后续几乎不会用到这个表里的数据,这样会将之前频繁被访问的缓存页挤到LRU尾部,这样显然不合理。
1.7 LRU算法优化
冷热数据分离,上面说的两个问题,其实不都是因为所有缓存页都在同一个LRU链表里面才会导致被加载一次就进入LRU头部的么?所以可以将LRU链表分为两个,一个存放冷数据,一个存放热数据,MYSQL中设计的冷热数据比例是由innodb_old_blocks_pct这个参数控制的,默认是37,也就是冷数据占比37%,如图所示:
数据页第一次被加载到缓存页的时候,缓存页会被放到冷数据区域的头节点,如果这个数据页在1S后又被访问到,才被认为后续可能会经常访问这个缓存页,就会将这个缓存页挪到热数据区域头部节点去。这个时间可以通过innodb_old_blocks)time参数进行控制,默认为1000,单位毫秒。
针对热数据区域进行优化,在热数据区域的缓存页,是不是只要被访问一次,就需要立马移动到头部?如果这样的话会导致链表中的指针频繁移动,影响性能,因此MYSQL在冷热数据分离的基础上又进行了一步优化:只有在热数据区域后3/4部分的缓存页被访问才会移动到头部去,如果是前1/4部分的缓存页,则不需移动,因为在热数据区域的头部的缓存页被淘汰的几率很小,这个优化可以尽可能减少链表中的节点移动,从而提升性能。