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长这样👇
Buffer Pool的管理
free 链表的管理
当一个页被加载到Buffer Pool时,怎么知道哪个缓存页时空闲可以放的呢?
我们可以用一个free链表解决这个问题,注意free链表连接的是控制块,再由控制块找到后面的缓存页。
一开始时,Buffer Pool上的所有控制块都在free链表上,当一个页被加载到Buffer Pool时,我们就从free链表移除一个节点,表示该节点代表的缓存页已经被使用了。
这里的基节点(包括后面介绍的基节点)是单独申请的一块内存空间。
哈希处理(缓存命中)
好了,我们现在已经知道一个页从磁盘加载到Buffer Pool该怎么放了,那怎么用到这个页呢?或者说,当MySQL程序读取某个页时,我们怎么知道它在Buffer Pool 还是 磁盘呢?
所以,我们还需要用一个哈希表记录已经在Buffer Pool的页了,key是表空间号+页号,value是缓冲页控制块!
每次MySQL程序读取页,会先到Buffer Pool中的哈希表查找是否有该键值对,有就缓存命中直接使用,没有再从磁盘加载到Buffer Pool读取👆
flush链表的管理
现在程序读取一个页已经没有问题了,但如果修改了Buffer Pool某个缓存页的数据,它与磁盘上的页就不一样了,这样的页叫做脏页,我们可以每当修改完某个缓冲页就刷回磁盘对应页,但频繁向磁盘写入数据会严重影响程序性能,所以我们一般先放着,等到某个时间点一起刷回(类似TCP中的Nagle算法)。
当然如果我们不立即刷回的话,就需要用一个链表记录哪些缓冲页是脏页,这个链表就是flush链表,它的结构和free链表基本一样👇
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_first
和t_forbidden
比较,小于就不挪到链表前部 ✅
当然这个链表也会有其它问题,这里就不多说了
刷新脏页到磁盘
后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求
从LRU链表尾部刷新一部分页到磁盘
缓冲页控制块里会记载该页是否被修改过的信息,后台线程定时从LRU链表尾部开始扫描,将一些脏页刷回磁盘,扫描的数量由innodb_lru_scan_depth
决定
从flush链表刷新一部分页面到磁盘
后台线程定时从flush链表刷新一些脏页到磁盘,刷新频率取决于当时系统是否繁忙
同步刷新
当新的页被加载到Buffer Pool,但内存空间不够时,会到LRU尾部查看是否有未修改的页直接释放,没有的话就只能同步刷新一个脏页到磁盘上(同步等待,异步不等待)
用户线程参与刷新
当系统特别繁忙时,用户线程也可能参与刷新,属于一种迫不得已的行为
参考资料:
- 《MySQL是怎样运行的》
Q.E.D.