参考:https://juejin.im/book/5bffcbc9f265da614b11b731/section/5c238f0851882521eb44c51f
即使只需要访问某个数据页中的一条记录,InnoDB也会把该记录所在的整个页中的所有数据加载到内存中,查询完成之后也并不急于释放该内存,而是将该页的数据缓存起来,在缓存该数据页时,InnoDB会向操作系统申请一块连续的内存空间作为缓存区,该缓存区就叫作Buffer Pool(中文名:缓冲池)。可以通过innodb_buffer_pool_size参数设置该缓存区的大小,默认为128M,最小5M。
Buffer Pool的组成示意图如下:控制块区+碎片区+缓存页区,Buffer Pool管理缓存页区的方式和磁盘管理数据页的方式是相同的,默认都是16KB,一个缓存也与一个控制块是一一对应的,控制块中存储着对应的缓存页的信息,比如该页(指在磁盘中存储着的数据页)属的表空间号、页号、该缓存页在Buffer Pool中地址、链表节点信息、锁信息和LSN信息。
free链表:由空闲控制块(空闲的控制块即指对应的缓存页空间还没有被使用)组成的链表,用来表明Buffer Pool中哪些缓存页是空闲可用的。每当从磁盘加载一个数据页到Buffer Pool时,就从free链表中取出一个空闲的缓存页,并将数据页对应的信息(如所属的表空间号、页号等信息)写入到对应的控制块中,然后从free链表中移除该节点。(free链表的结构示意图请参考lru链表)
flush链表:如果缓存页中的数据被修改,在被同步刷新到磁盘之前,这些被更新的数据称为脏数据,这些被更新的缓存页被称为脏页,由这些脏页所对应的控制块组成的链表叫作flush链表,其组成结构和示意图与free链表相同。(free链表的结构示意图请参考lru链表)
lru链表:Buffer Pool的内存空间是有限的,当Buffer Pool中的缓存页都已经被使用完时,继续加载新的数据页到Buffer Pool中时就需要淘汰Buffer Pool中的部分缓存页,lru链表就是按照”最近最少使用“原则对缓存页进行淘汰。lru链表的组成和示意图与free链表和flush链表的相同。访问某个数据页(即查询数据)时,LRU链表的处理逻辑如下:
- 如果该数据页不在Buffer Pool中,那么就从磁盘中将该数据页加载到Buffer Pool的缓存页中,并将该缓存页对应的控制块移到lru链表的头部;
- 如果该数据页已经被缓存到Buffer Pool中,则将该缓存页对应的控制块移到lru链表的头部。
为了解决Buffer Pool的缓存命中率可能会较低的情况(InnoDB的预读机制和全表扫描都会导致缓存命中率降低),按照缓存页数据被访问到的频次将lru链表分为两个区域:young区域和old区域,young区域控制块对应的缓存页中的数据是访问频率比较高的区域,这部分数据叫热数据;old区域缓存快对应的缓存页中的数据是访问频次较低的区域,这部分数据叫冷数据,示意图如下。young区域和old区域按照一定的比例将lru链表切分成两段,这个比例值使用系统变量innodb_old_blocks_pct进行设置。磁盘中的数据页首次被加载到Buffer Pool时会先放到old区域的头部,如果该缓存页再次被访问到时的时间间隔大于innodb_old_blocks_time的值时(该值的设定是为了防止全表扫描时不常用到的数据页被加载到Buffer Pool从而淘汰掉使用率较高的缓存页),该缓存页对应的控制块会被移动到young区域的头部。
刷新脏页到磁盘:后台有专门的线程负责每隔一段时间把脏页刷新到磁盘,该过程为异步过程,所以不会影响用户线程的正常运行。刷新脏页有两种方式:
- 从lru链表的冷数据中刷新一部分页面到磁盘中;
- 从flush链表中刷新一部分脏页到磁盘中。
有时后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个数据页到Buffer Pool中时没有缓存页可用,此时用户线程会查看lru链表尾部中有没有未修改的页面(即不是脏页,脏页不能直接释放)可以直接释放,如果没有(即LRU链表尾部的缓存页都是脏页),用户线程会先将LRU链表尾部的单个脏页同步刷新到磁盘然后释放该脏页所占用的缓存页空间,最后再将要访问的数据页加载到该释放的缓存页中,这样的一个过程会导致用户线程的速度特别慢,因为用户线程同步刷新脏页到磁盘需要与磁盘进行交互,和磁盘交互的速度特别慢。