在innodb
引擎当中,数据和索引都是以页的形式存在表空间当中,表空间只不过是InnoDB对文件系统上一个或几个实际文件的抽象。所以数据最终还是存在磁盘上面,为了匹配上cpu的处理速递,InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了。
innoDB为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,叫做Buffer Pool(名是缓冲池)。
默认情况下Buffer Pool只有128M
大小。可以在启动服务器的时候配置innodb_buffer_pool_size
参数的值,它表示Buffer Pool的大小:
[server]
innodb_buffer_pool_size = 1024000000
Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB。为了更好的管理这些在Buffer Pool中的缓存页,InnoDB为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息,还有一些别的控制信息。
每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:
可以看到控制块和缓存页之间有一个碎片,每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,这个用不到的那点儿内存空间就被称为碎片了。当然,如果把Buffer Pool的大小设置的刚刚好的话,也可能不会产生碎片
备注:每个控制块大约占用缓存页大小的5%,在MySQL5.7.21这个版本中,每个控制块占用的大小是808字节。而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,也就是说InnoDB在为Buffer Pool向操作系统申请连续的内存空间时,这片连续的内存空间一般会比innodb_buffer_pool_size的值大5%左右。
最初启动Mysql服务器的时候,向操作系统申请到了Buffer Pool
的内存空间,然后把它分为若干个控制块和缓存页。然后随着程序的运行,不断的有磁盘上的页被缓存到Buffer Pool中,这时为了区分Buffer Pool
中空闲的缓存页,缓存页对应的控制块就起了作用。
innodb把所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,这个链表就称为free 链表
。
有了free 链表
之后,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了
如果我们修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页
。
如果我们一做出修改就同步到磁盘上,这样性能就很低。所以每次修改缓存页后,不会立即同步,而是在未来某个时间点进行同步。
但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道Buffer Pool中哪些页是脏页,哪些页从来没被修改过呢?所以,innodb会创建一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会作为一个节点加入到一个链表中,因为这个链表节点对应的缓存页都是需要被刷新到磁盘上的,所以也叫flush链表
。
Buffer Pool
内存容量有限,所以使用创建来一个LRU链表
来淘汰旧的缓存页,释放空间。
简单的LRU链表处理如下:
- 如果该页不在
buffer pool
, 当从磁盘中加载该页的时候,就把该页对应的控制块加载到链表头部 - 如果已经缓存在
buffer pool
了,就直接将控制块移动到LRU链表
头部
首先描述两个情况:预读和大量加载页
InnoDB提供了一个比较贴心的服务——预读(read ahead)。所谓预读就是InnoDB认为执行当前的请求可能之后会读取某些页面,就预先把它们加载到Buffer Pool中。根据触发方式的不同,预读又可以细分为下边两种:
- 线性预读
innoDB提供了一个系统变量innodb_read_ahead_threshold
,如果顺序访问了某个区(extent
,表空间由段组成,段又由区组成)的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到Buffer Pool
的请求,注意异步读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。这个innodb_read_ahead_threshold
系统变量的值默认是56
,可以在服务器启动时通过启动参数或者服务器运行过程中直接调整该系统变量的值.
- 随机预读
如果Buffer Pool
中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步
读取本区中所有其的页面到Buffer Pool的请求。InnoDB同时提供了innodb_random_read_ahead
系统变量,它的默认值为OFF,也就意味着InnoDB并不会默认开启随机预读的功能,如果我们想开启该功能,可以通过修改启动参数或者直接使用SET GLOBAL命令把该变量的值设置为ON
预读本来是个好事儿,如果预读到Buffer Pool中的页成功的被使用到,那就可以极大的提高语句执行的效率。可是如果用不到呢?这些预读的页都会放到LRU链表的头部,但是如果此时Buffer Pool的容量不太大而且很多预读的页面都没有用到的话,这就会导致处在LRU链表尾部的一些缓存页会很快的被淘汰掉,会大大降低缓存命中率
如果进行全表扫描,一次读入大量的页,这样就会有大量的页写入buffer poll
,等于buffer pool
中所有的页换了一次,执行其他查询的时候又要重新载入。但是全表扫描语句执行的频率也不高,严重影响到了其他查询使用buffer pool
,大大的降低了缓存命中率
针对上述两种情况,innodb将lru按照一定比例分为了两部分:
- 一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做
热数据
,或者称young
区域 - 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做
冷数据
,或者称old
区域
这个区域对划分是按照某个比例,通过innodb_old_blocks_pct
的值来确定old区域在LRU链表中所占的比例。这个值默认是37
,可以通过启动参数修改。
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。主要有两种刷新路径:
- 从LRU链表中刷新一部分页面到磁盘:
后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth
来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU
- 从flush链表中刷新一部分页面到磁盘:
后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST
。