InnoDB的Buffer Pool

MySQL磁盘与CPU之间的缓存,一块连续的内存

为什么要有Buffer Pool?

我们都知道,使用InnoDB存储引擎的表数据都是以页的形式存储在表空间,而表空间是以.ibd文件存储在磁盘上的。每次读取数据都要从磁盘上加载一个页到内存,如果我们没有用Buffer Pool缓存这些页,下次读取就还要重复这个过程,磁盘IO可太慢了呀🤣

Buffer Pool长啥样?

Buffer Pool是MySQL服务器启动时向操作系统申请的一块连续内存,用来缓存磁盘读取的页,默认情况下128MB,可以在启动设置 innodb_buffer_pool_size = 字节数

Buffer Pool这块内存被分成若干个页面,每个页面和磁盘的页大小一样大,默认都是16KB,这里我们叫缓冲页,每个缓存页都配置一个控制块,负责记录页所属的表空间、页号啥的,这些控制块大小都一样,统一放到前面,所以Buffer Pool长这样👇

image-20220306174017408

Buffer Pool的管理

free 链表的管理

当一个页被加载到Buffer Pool时,怎么知道哪个缓存页时空闲可以放的呢?

我们可以用一个free链表解决这个问题,注意free链表连接的是控制块,再由控制块找到后面的缓存页。

一开始时,Buffer Pool上的所有控制块都在free链表上,当一个页被加载到Buffer Pool时,我们就从free链表移除一个节点,表示该节点代表的缓存页已经被使用了。

image-20220306174844447

这里的基节点(包括后面介绍的基节点)是单独申请的一块内存空间。

哈希处理(缓存命中)

好了,我们现在已经知道一个页从磁盘加载到Buffer Pool该怎么放了,那怎么用到这个页呢?或者说,当MySQL程序读取某个页时,我们怎么知道它在Buffer Pool 还是 磁盘呢?

所以,我们还需要用一个哈希表记录已经在Buffer Pool的页了,key是表空间号+页号,value是缓冲页控制块!

每次MySQL程序读取页,会先到Buffer Pool中的哈希表查找是否有该键值对,有就缓存命中直接使用,没有再从磁盘加载到Buffer Pool读取👆

flush链表的管理

现在程序读取一个页已经没有问题了,但如果修改了Buffer Pool某个缓存页的数据,它与磁盘上的页就不一样了,这样的页叫做脏页,我们可以每当修改完某个缓冲页就刷回磁盘对应页,但频繁向磁盘写入数据会严重影响程序性能,所以我们一般先放着,等到某个时间点一起刷回(类似TCP中的Nagle算法)。

当然如果我们不立即刷回的话,就需要用一个链表记录哪些缓冲页是脏页,这个链表就是flush链表,它的结构和free链表基本一样👇

image-20220307092535297

LRU链表的管理

Buffer Pool是一块内存空间,每次程序读取页都会从磁盘加载到这里,如果不进行空间管理的话,只进不出,迟早会占满!

所以我们需要制定页的淘汰策略,第一时间想到的就是LRU链表,不过也没那么简单,我们来看看会遇到哪些问题,又该如何解决👇

简单的LRU链表

练习题链接👉 LRU 缓存

只要我们用到某个缓冲页,就把它(的控制块)调到LRU链表的头部,当内存不够时,从队尾淘汰缓冲页。

当然,这样会遇到一下问题👇

  • 预读:InnoDB认为执行某次操作后,可能会在后面读取某些页,于是预先加载到Buffer Pool中
  • 全表扫描:查询的访问方式为all,即需要读取全表所有记录

这两个都会造成一些实际并没有经常使用的页被放到LRU链表的头部,将那些真正高频率使用的页挤到尾部被淘汰,造成Buffer Pool缓存命中率低

分区域的LRU链表

我们可以在上面的LRU基础上优化,将LRU链表分为两段,前段为young区,后段为old

  • 解决预读
    预读加载的页放入old区,后续不访问的页会在old区里慢慢被淘汰,用到的页会被放到young区中 ✅
  • 解决全表扫描
    全表扫描加载页到old区后,很快就会读取这个页的所有记录,没读取一次记录都相当于访问这个页一次,所以很快就会全部挤到young区前部;这就像是有人在网站上刷屏,我们只需要设置一个禁止时间,让他等一段时间才能发下一条;对应到LRU就是,设置一个时间间隔t_forbidden(默认1s),第一次访问页时在控制块记录下这个时间t_first,后续访问的时间t - t_firstt_forbidden比较,小于就不挪到链表前部 ✅

当然这个链表也会有其它问题,这里就不多说了

刷新脏页到磁盘

后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求

从LRU链表尾部刷新一部分页到磁盘

缓冲页控制块里会记载该页是否被修改过的信息,后台线程定时从LRU链表尾部开始扫描,将一些脏页刷回磁盘,扫描的数量由innodb_lru_scan_depth决定

从flush链表刷新一部分页面到磁盘

后台线程定时从flush链表刷新一些脏页到磁盘,刷新频率取决于当时系统是否繁忙

同步刷新

当新的页被加载到Buffer Pool,但内存空间不够时,会到LRU尾部查看是否有未修改的页直接释放,没有的话就只能同步刷新一个脏页到磁盘上(同步等待,异步不等待)

用户线程参与刷新

当系统特别繁忙时,用户线程也可能参与刷新,属于一种迫不得已的行为


参考资料:

  • 《MySQL是怎样运行的》

Q.E.D.


记录 • 分享 • 日常