这部分主要跟着大佬的脚步“MySQL 是怎样运行的:从根儿上理解 MySQL”,学习MySQL的Buffer Pool(缓冲池)。
InnoDB 的 Buffer Pool(缓冲池)
由于CPU上的速度远远大于磁盘速度, 需要先将数据加载到内存中进行读写访问, 在读写完成后不急着将该页对应的内存空间释放掉, 而是将其缓存起来, 将来如果有请求再次访问该页面时, 可以省区磁盘的IO开销。
通过启动服务器的时候配置 innodb_buffer_pool_size
参数值, 表示 Buffer Pool的 大小。
每一个缓存页都创建了一些所谓的控制信息
,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息等等。 每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块
, 大小为808B, 大概是一个缓存页(16KB)的5%。 下面时如何管理控制块
。申请Buffer Pool 时需要额外申请5%大小控件
Buffer Pool向操作系统申请的连续内存由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后,Buffer Pool剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为碎片。
为了快速定位某个页是否被加载到Buffer Pool,使用表空间号 + 页号作为key,缓存页作为value,建立哈希表。
free 链表
所有空闲的缓存页对应的控制块作为一个节点放到一个链表中
flush 链表
脏页 与磁盘上页不一致 后台有专门的线程每隔一段时间负责把脏页刷新到磁盘
从LRU链表的冷数据中刷新一部分页面到磁盘。
后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU。从flush链表中刷新一部分页面到磁盘。
后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST。
LRU链表(Least Recently Used)
- 根据
缓存命中率
(假设我们一共访问了n次页,那么被访问的页已经在缓存中的次数除以n就是所谓的缓存命中率), 在free链表没有多余的空闲缓存页时移除旧的缓存页。
简单的LRU链表实现
- 如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页对应的控制块作为节点塞到链表的头部。
- 只要我们使用到某个缓存页,就把该缓存页调整到LRU链表的头部,这样LRU链表尾部就是最近最少使用的缓存页,Buffer Pool中的空闲缓存页使用完时,到LRU链表的尾部找些缓存页淘汰。
简单LRU实现可能存在的问题 - 加载到Buffer Pool中的页不一定被用到。(可能因为预读,导致一些数据被塞到链表头部,但可能一直用不到) - 如果非常多的使用频率偏低的页被同时加载到Buffer Pool时,可能会把那些使用频率非常高的页从Buffer Pool中淘汰掉。(例如扫描全表的查询语句)
划分区域的LRU链表实现
- 部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。
- 另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。
系统变量innodb_old_blocks_pct
的值来确定old区域在LRU链表中所占的比例
SET GLOBAL innodb_old_blocks_pct = 40;
- 针对预读的页面可能不进行后续访情况的优化
当磁盘上的某个页面在初次加载到Buffer Pool中的某个缓存页时,该缓存页对应的控制块会被放到old区域的头部。这样针对预读到Buffer Pool却不进行后续访问的页面就会被逐渐从old区域逐出,而不会影响young区域中被使用比较频繁的缓存页。 - 针对全表扫描, 短时间内访问大量使用频率非常低的页面情况的优化
首次被加载到Buffer Pool的页被放到了old
区域的头部,一个页面有很多条记录,在全表扫描时, 该页会被反复访问,为了防止这种页被放到young
区域头部,所以规定: 在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。
上述的这个间隔时间是由系统变量innodb_old_blocks_time控制的SHOW VARIABLES LIKE 'innodb_old_blocks_time'
更进一步优化LRU链表
频繁移动缓存页到LRU链表头部。 实际上开销很大, young
区域作为热点数据,更是被经常访问。所以需要想办法降低陶正LRU链表的频率, 最终的目标是 尽量高效的提高 Buffer Pool 的缓存命中率
- 例如某个缓存页对应的节点在young区域的1/4中,再次访问该缓存页时也不会将其移动到LRU链表头部