执行CRUD都会将磁盘数据页加载到缓存页,那在加载数据到缓存页时,必然是要加载到空闲缓存页,所以必须要从free中找个空闲缓存页,然后把磁盘数据页加载到该空闲缓存页
随着不断将磁盘数据页加载到空闲缓存页,free中的空闲缓存页会越来越少。最终耗尽free中的空闲缓存页。这时,还要加载数据页到一个空闲缓存页时,MySQL 该何去何从?
若所有缓存页都有数据了,那就无法再从磁盘加载新数据页到缓存页了,则只能淘汰一些缓存页:把一个缓存页里被修改过的数据,刷到磁盘的数据页,然后该缓存页就能被清空, 变回空闲页。然后就能将磁盘的新数据页加载到这刚腾出的空闲页:
那应该把哪个倒霉的缓存页的数据刷盘呢?
现有两个缓存页:
很显然,作为领导的你,肯定想把第二个员工裁了吧?
如何知晓哪些缓存页经常被访问,哪些缓存页很少被访问?
这就需要LRU,Least Recently Used,最近最少使用。这样当缓存页需空出一个刷盘时,通过LRU链表,就能知道最近最少被使用的缓存页。
假设从磁盘加载一个数据页到缓存页时,就将该缓存页的描述信息块放入LRU链表头部,那么只要有数据的缓存页,他都会在LRU里,最近被加载数据的缓存页,都会放到LRU链表头部。
假设某缓存页的描述信息块本在LRU链尾,后续你只要查询或修改了该缓存页数据,也要将这缓存页移到LRU链头,即最近被访问过的缓存页,一定在LRU链头。
如此,当无空闲缓存页时候,就能轻易找出最近最少被访问的缓存页去刷盘,即LRU链尾的缓存页,将其刷盘,然后把你需要的磁盘数据页加载到这刚空出的缓存页。
但这样的LRU实际运行时会有问题。
当你从磁盘加载一个数据页时,他可 能会连带着把该数据页相邻的其他数据页,也加载到缓存。
现有两个空闲缓存页,加载一个数据页时,连带着把他的一个相邻数据页也加载到缓存,正好每个数据页放入一个空闲缓存页!
然后呢?实际上只有一个缓存页被访问,另外一个通过预读机制加载的缓存页,其实无人问津,此时这俩缓存页可都在LRU链表前边:
这时,若无空闲页了,要加载新数据页,就得从LRU链表的尾部将“最近最少使用的缓存页”取出,刷入磁盘,就空出一个缓存页了。
若选择将上图中LRU尾部那个缓存页刷盘,然后清空,合理吗?
他可是之前一直频繁被访问呀,只是这一瞬间,被新加载进的两个缓存页给占了LRU链表前面的位置,尤是第二个缓存页,居然还是通过预读加载来的,其实根本无人访问!而这时将LRU链表尾部缓存页刷盘,肯定不合理,最合理的反而是将那LRU链表第二个通过预读机制加载进的缓存页给淘汰。
所以默认主要第一个规则可能触发预读,一下将很多相邻区里的数据页加载进缓存,这些缓存页若突然都放在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链表前面部分,然而可能这些预读的数据页压根儿后续无人用,那这预读机制对性能不增反减。
于是,为了解决前面的问题,真正MySQL采取冷热数据分离思想改良了 LRU。
之前问题都是因为所有缓存页都混在一个LRU链表才导致的,改良版LRU链表拆为热数据、冷数据两部分,冷热数据比例由innodb_old_blocks_pct参数控制,默认37,即冷数据占37%。这时的LRU链表:
数据页第一次被加载到缓存时,缓存页会被放在冷区的链表头部。
第一次被加载了数据的缓存页都会不停移动到冷区的链表头部。那为何不放到热区头部呢?
你刚加载了一个数据页到那个缓存页,他在冷区的链表头部,然后立马(在1ms以内)就又被访问了,但之后就再也不访问了呢?难道这种情况也要把这缓存页放到热区头部吗?
所以MySQL设innodb_old_blocks_time参数,默认1000,即1000ms:一个数据页被加载到缓存页之后,在1s后,你又访问了该缓存页,他才会被移到热区的链表头部。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://javaedge.blog.csdn.net/article/details/122788493
内容来源于网络,如有侵权,请联系作者删除!