Buffer Pool缓存页不够时,如何淘汰缓存?
若BP缓存页不够了,咋办?
执行CRUD都会将磁盘数据页加载到缓存页,那在加载数据到缓存页时,必然是要加载到空闲缓存页,所以必须要从free中找个空闲缓存页,然后把磁盘数据页加载到该空闲缓存页
随着不断将磁盘数据页加载到空闲缓存页,free中的空闲缓存页会越来越少。最终耗尽free中的空闲缓存页。这时,还要加载数据页到一个空闲缓存页时,MySQL 该何去何从?
若所有缓存页都有数据了,那就无法再从磁盘加载新数据页到缓存页了,则只能淘汰一些缓存页:把一个缓存页里被修改过的数据,刷到磁盘的数据页,然后该缓存页就能被清空, 变回空闲页。然后就能将磁盘的新数据页加载到这刚腾出的空闲页:
那应该把哪个倒霉的缓存页的数据刷盘呢?
缓存命中率
现有两个缓存页:
- 一个缓存页的数据,经常被修改和查询,都可以操作缓存,不需要从磁盘加载数据,这那缓存命中率就很高。这种高级员工就是啥脏活累活,都会接受。
- 另一个缓存页里的数据,刚从磁盘加载到缓存页后,被修改和查询过1次,之后100次请求再没有一次是修改和查询该缓存页数据的,那这缓存命中率就有点低了,因为大部分请求还是走磁盘查询数据,他们要操作的数据不在缓存。这种高级员工啥事都干不了,都还得交给低级员工们干事。
很显然,作为领导的你,肯定想把第二个员工裁了吧?
引入LRU,判断哪些缓存页不常用
如何知晓哪些缓存页经常被访问,哪些缓存页很少被访问?
这就需要LRU,Least Recently Used,最近最少使用。这样当缓存页需空出一个刷盘时,通过LRU链表,就能知道最近最少被使用的缓存页。
LRU工作原理
假设从磁盘加载一个数据页到缓存页时,就将该缓存页的描述信息块放入LRU链表头部,那么只要有数据的缓存页,他都会在LRU里,最近被加载数据的缓存页,都会放到LRU链表头部。
假设某缓存页的描述信息块本在LRU链尾,后续你只要查询或修改了该缓存页数据,也要将这缓存页移到LRU链头,即最近被访问过的缓存页,一定在LRU链头。
如此,当无空闲缓存页时候,就能轻易找出最近最少被访问的缓存页去刷盘,即LRU链尾的缓存页,将其刷盘,然后把你需要的磁盘数据页加载到这刚空出的缓存页。
表和行等概念和表空间、数据页的关系
- 表、列和行,都是逻辑概念,我们只关注DB里有一个表,表里有几个字段,有多少行,但是这些表里的数据
- 表空间、数据页等是物理概念,在物理层面,表里的数据都放在一个表空间,表空间由一堆磁盘上的数据文件组成,这些数据文件里都存放了表中的数据,这些数据由一个个数据页组织起来
但这样的LRU实际运行时会有问题。
预读
当你从磁盘加载一个数据页时,他可 能会连带着把该数据页相邻的其他数据页,也加载到缓存。
现有两个空闲缓存页,加载一个数据页时,连带着把他的一个相邻数据页也加载到缓存,正好每个数据页放入一个空闲缓存页!
然后呢?实际上只有一个缓存页被访问,另外一个通过预读机制加载的缓存页,其实无人问津,此时这俩缓存页可都在LRU链表前边:
这时,若无空闲页了,要加载新数据页,就得从LRU链表的尾部将“最近最少使用的缓存页”取出,刷入磁盘,就空出一个缓存页了。
若选择将上图中LRU尾部那个缓存页刷盘,然后清空,合理吗?
他可是之前一直频繁被访问呀,只是这一瞬间,被新加载进的两个缓存页给占了LRU链表前面的位置,尤是第二个缓存页,居然还是通过预读加载来的,其实根本无人访问!而这时将LRU链表尾部缓存页刷盘,肯定不合理,最合理的反而是将那LRU链表第二个通过预读机制加载进的缓存页给淘汰。
MySQL预读触发时机
参数innodb_read_ahead_threshold默认56,即若顺序访问了一个区里的多个数据页,访问的数据页数量超过阈值,就会触发预读,将下个相邻区中的所有数据页都加载到缓 存
若BP里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时直接触发预读,把这个区里的其他的数据页都加载到缓存里去。该机制通过参数innodb_random_read_ahead控制,默认OFF关闭。
所以默认主要第一个规则可能触发预读,一下将很多相邻区里的数据页加载进缓存,这些缓存页若突然都放在LRU链表前面,且他们其实并没啥人访问,就会如上图,导致本就在缓存里的一些频繁被访问的缓存页却在LRU链尾。后续一旦要淘汰缓存页,就会将链尾的一些频繁被访问的缓存页给淘汰!
全表扫描
如:
SELECT * FROM xxx
一下子就将表里所有数据页都从磁盘加载到BP。这时他可能会一下子就把这个表的所有数据页都装入各缓存页。此时可能LRU链表中排在前面的一大串缓存页,都是全表扫描加载进来的。若此次全表扫描后,后续几乎没用到这个表里的数据呢?此时LRU链尾可能全都是之前一直被频繁访问的那些缓存页!
然后当需要淘汰缓存页时,就会将LRU链表尾部一直被频繁访问的缓存页给淘汰掉了,而留下之前全表扫描加载进来的大量的不经常访问的缓存页。
为何MySQL设计预读机制,为何有时要把相邻的一些数据页一次性读入到Buffer Pool缓存?
为提升性能。假设你读取了数据页01到缓存页里去,那接下来有可能会接着顺序读取数据页01相邻的数据页02到缓存页,是不是可能在读取数据页02的时候要再次发起一次磁盘IO?
所以为优化性能,MySQL设计了预读机制,即若在一个区内,你顺序读取了好多数据页,比如数据页01~56都被你依次顺序读取了,MySQL觉得你可能接着会继续顺序读取后面的数据页。
此时他干脆提前把后续一大堆数据页(如数据页57~72)都读取到Buffer Pool,后续你再读取数据页60时,就能直接从Buffer Pool里拿到。
但现实骨感,预读的一大堆数据页要是占据LRU链表前面部分,然而可能这些预读的数据页压根儿后续无人用,那这预读机制对性能不增反减。
冷热分离的LRU
于是,为了解决前面的问题,真正MySQL采取冷热数据分离思想改良了 LRU。
之前问题都是因为所有缓存页都混在一个LRU链表才导致的,改良版LRU链表拆为热数据、冷数据两部分,冷热数据比例由innodb_old_blocks_pct参数控制,默认37,即冷数据占37%。这时的LRU链表:
数据页第一次被加载到缓存时,缓存页会被放在冷区的链表头部。
冷区缓存页何时放入热区?
第一次被加载了数据的缓存页都会不停移动到冷区的链表头部。那为何不放到热区头部呢?
你刚加载了一个数据页到那个缓存页,他在冷区的链表头部,然后立马(在1ms以内)就又被访问了,但之后就再也不访问了呢?难道这种情况也要把这缓存页放到热区头部吗?
所以MySQL设innodb_old_blocks_time参数,默认1000,即1000ms:一个数据页被加载到缓存页之后,在1s后,你又访问了该缓存页,他才会被移到热区的链表头部。