InnoDB存储引擎在处理客户端请求时如果要访问某个页的数据,就会把完整的页加入到内存中,然后进行读写访问,在读写访问后并不急着将其对应的内存空间释放掉,而是将其缓存起来,将来有新的请求访问页面时,就可以省下磁盘IO开销了。

InnoDB在Mysql启动时申请了一片连续的内存来缓存磁盘中的页,该内存就称为Buffer Pool(缓冲池)。

默认情况下,Buffer Pool只有128MB,可以在服务器启动时配置innodb_buffer_pool_size 设置Buffer Pool的大小,单位是字节

1、Buffer Pool的内部组成

点击查看【processon】

2、free链表

为了区分Buffer Pool中哪些缓冲页是空闲的,哪些缓冲页是已经使用的,InnoDB设计了空闲链表。

将空闲缓冲页对应的控制块作为节点放到一个链表中,这个链表被称为free链表(空闲链表)

点击查看【processon】
从磁盘加载页到Buffer Pool中的流程:
从空闲链表中取一个空闲的缓冲页,并将缓冲页对应控制块的信息填上(即该页所在表空间,页号等信息),然后将缓冲页对应的控制块节点从空闲链表中移除,就表示该缓冲页已经使用了。

3、缓冲页的哈希处理

如何快速判断磁盘的页是否存在于Buffer Pool中呢?

使用表空间 + 页号作为key,缓冲页控制块的地址作为value创建一个哈希表,当需要访问某个页的数据时,先根据哈希表根据表空间号 + 页号查找是否有对应的缓冲页,如果有,则直接使用缓冲页进行操作,如果没有就从链表中选一个空闲的缓冲页,将磁盘中的页加载到缓冲页中。

4、flush链表

当修改了Buffer Pool中某个缓冲页的数据,该缓冲页就与磁盘上的页数据不一致了,这样的页称为脏页。

为了区分哪些缓冲页是脏页,InnoDB设计了存储脏页的链表,凡是被修改缓冲页对应的控制块都会作为一个节点加入到该链表中,因为该链表节点对应的缓冲页是要被刷新到磁盘上的,所以也称为flush链表

点击查看【processon】

5、LRU链表的管理

Buffer Pool内存空间有限,当需要从磁盘加载页面导Buffer Pool页面发现free链表上没有空闲缓冲页时,需要将某些旧的缓冲页从Buffer Pool移除,那么移除哪些缓冲页呢?

5.1、简单的LRU链表方案

简单的LRU链表方案:
创建一个链表,按照最近最少使用的原则去淘汰缓冲页,该链表称为LRU链表。
当要访问磁盘的某个页时,按如下方法处理LRU链表
1、如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool 的缓冲页是,就把该缓冲页对应的控制块作为节点加到LRU链表的头部。
2、如果该页已经加载到BufferPool中,则直接吧该页对应的控制块移动到LRU链表的头部。

这样LRU链表尾部就是最近最少使用的缓冲页,当Buffer Pool中的空闲缓冲页用完时,就到LRU链表尾部淘汰一些缓冲页。

简单的LRU链表方案的问题:

mysql提供了预读功能,当InnoDB认为执行当前请求时,可能会在后面读取某些页面,于是就预先将页面加载到Buffer Pool中 线性预读:如果顺序访问某个区的页面超过了系统变量innodb_read_ahead_threshold(默认56)的值,就会触发一次异步读取下一个区中的全部页面到BufferPool中的请求。 随机预读:如果某个区的13个连续页面都被加载到了Buffer Pool中,无论这些页面是不是顺序读取的,都会触发一次异步读取本区所有其它页面到Buffer Pool中的请求。(随机预读默认关闭,由系统变量innodb_random_read_ahead控制,默认为OFF)

1、因为预读机制,很多预读的页都会放到LRU链表的头部,如果此时Buffer Pool内存不足,而且很多预读的页面没有用到的话,就会导致LRU链表尾部的一些缓冲页被淘汰掉,从而大大降低Buffer Pool的命中率。
2、执行了全表扫描的语句,极端情况下,如果全表查询的表很大,那么BufferPool的缓存页可能会全表替换一次,这回严重影响到其它查询Buffer Pool的命中率。

5.2、InnoDB的LRU链表方案

因为简单的LRU链表方案存在的问题,InnoDB设计了新的LRU实现。

将LRU链表按照一定比例分为两截:
一部分存储使用频率非常高的缓冲页,这一部分链表也称为热数据或者young区域
另一部分存储使用频率不是很高的缓冲页,这一部分也被称为冷数据或者old区域

点击查看【processon】
热数据和冷数据部分的划分比例由系统变量innodb_old_blocks_pct控制,默认是37%

**show VARIABLES like 'innodb_old_blocks_pct'**
image.png

访问页面规定:
当从磁盘加载页面到BufferPool缓冲区时,该缓冲区页面的控制块会放在old区域的头部。
当对old区域的缓冲页进行第一次访问时,在其对应的控制块中记录访问时间,如果后续的访问与第一次访问的时间在某个时间间隔内(系统变量**innodb_old_blocks_time**控制,默认1000ms),那么该页面就不会从old区域移到young区域,否则将其对应的控制块移到young区域的头部。

第一条可以优化预读页面不能进行后续访问的问题,第二条可以优化全表扫描,短时间内大量访问使用频率非常低的页面。

6、刷新脏页到磁盘

后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘,刷新的位置来源主要是以下两种:

  • 从LRU链表的冷数据中刷一部分页面到磁盘
  • 从flush链表中刷一部分页面到磁盘

刷新方式:
BUF_FLUSH_LRU:
后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的数量可以通过系统变量innodb_lru_scan_depth(默认1024)指定,如果在lru链表中发现脏页,则将其刷新到磁盘
BUF_FLUSH_LIST:
后台线程定时从flush链表刷新一部分页面到磁盘,刷新速率取决于当时系统是否繁忙
BUF_FLUSH_SINGLE_PAGE
后台刷新脏页的速度较慢,导致用户线程在加载一个磁盘页面到Buffer Pool时,没有可用的空闲缓冲页,此时会尝试查看LRU链表的尾部,看看是否存在可以直接释放的未修改缓冲页,如果没有,不得不将LRU链表尾部的一个脏页同步刷新到磁盘。

7、多个Buffer Pool实例

在多线程环境下,访问Buffer Pool的各种链表都需要加锁处理,在Buffer Pool特别大并且多线程并发访问量特别高的情况下,单一的Buffer Pool可能会影响请求的处理速度,所以在Buffer Pool特别大时,可以拆分成多个小的Buffer Pool实例。多个小的Buffer Pool实例都是独立的,独立的申请内存空间,独立的管理各种链表,在多线程并发访问时不会相互影响,从而提高了并发处理能力。
可以在服务器启动时通过设置**innodb_buffer_pool_instances**的值来修改Buffer Pool实例的个数。

并不是说Buffer Pool实例创建的越多越好,分别管理Buffer Pool也是需要性能开销的,于是规定当**innodb_buffer_pool_size** 小于1GB时,设置多个实例时无效的,innoDB会默认把**innodb_buffer_pool_instances**的值修改为1

Mysql5.7.5之前,只能在服务器启动时通过配置**innodb_buffer_pool_size**的值来调整Buffer Pool的大小。
在Mysql 5.7.5之后,InnoDB不再为某个Buffer Pool实例向操作系统申请一大片连续的内存空间,而是以一个chunk为单位向操作系统申请内存空间。也就是说Buffer Pool实例其实是有若干个chunk组成,每个chunk代表一片连续内存,里面包含了若干缓冲页和其对应的控制块。

点击查看【processon】

chunk大小是由 **innodb_buffer_pool_chunk_size**指定的默认值为128MB,服务器运行过程中不可修改