MySQL - Buffer Pool LRU机制

MySQL - Buffer Pool LRU机制,第1张

LRU机制在实际运行过程中,是会存在巨大的隐患的:

MySQL的预读机制带来的隐患:所谓的预读机制,就是当你从磁盘加载一个数据页的时候,可能会连带着把这个数据页相邻的其它数据页也加载到缓存里去。

例如,现在有两个空闲的缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓存里了,正好每个数据页放入一个空闲的缓存页。但是实际上只有一个缓存页被访问了,另外一个通过预读机制被加载进来的缓存页,其实并没有人访问,但是此时这两个缓存页都是放在LRU链表的前面。这个时候没有空闲的缓存页,如果要加载新的数据,就要把LRU链表队尾的缓存页刷入磁盘,而不是无人访问的那个缓存页。

会触发预读机制的场景:

为了解决简单的LRU链表的问题,MySQL在设计LRU链表的时候,实际上采取的是冷热数据分离的思想;之前的问题都是因为所有缓存页都混在了一个LRU链表中导致的。

真正的LRU链表,会被拆分成热数据和冷数据两个部分,冷热数据的比例是由innodb_old_blocks_pct参数控制的,默认是37,也就是说冷数据占比为37%。这个时候,LRU链表实际上看起来是下面这个样子的:

数据页第一次被加载到缓存的时候,其实是被放在冷数据链表的头部,后面1秒之后,如果你再次访问这个缓存页,那这个缓存页会被移动到热数据链表的头部,这个时间是有innodb_old_blocks_time这个参数控制的,默认1000ms。

也就是说,必须是一个数据页被加载到缓存页之后,在1s之后,你访问这个缓存页,他才会被挪动到热数据区域的链表头部去。

因为假设你加载了一个数据页到缓存去,然后过了1s之后你还访问了这个缓存页,说明你后续很可能会经常访问它,这个时间限制就是1s,因此只有1s后你访问了这个缓存页,他才会给你把缓存页放到热数据区域链表的头部去。

预读机制以及全表扫描加载进来的一大堆缓存页,他们会放在哪里?肯定是放在LRU链表的冷数据区域的前面,假设这个时候热数据区域已经有很多被频繁访问的缓存页了,你会发现热数据区域还是存放被频繁访问的缓存页的,只要热数据区域有缓存页被访问,他还是会被移动到热数据区域的链表头部去。

所以此时预读机制和全表扫描加载进来的一大堆缓存页,此时都在冷数据区域里,跟热数据区域里的频繁访问的缓存页,没有关系。

如果仅仅是全表扫描的查询,此时你肯定是在1s内就把一大堆缓存页加载进来,然后访问了这些缓存页一下,通常这些 *** 作1s内就结束了,所以基于目前的一个机制,可以确定的是,那些缓存页是不会从冷数据区域转移到热数据区域的。

除非在冷数据区域的缓存页,在1s之后还被访问了,那么此时他们就会判定为未来可能会被频繁访问的缓存页,然后移动到热数据区域的链表头部去。

假设此时缓存页不够用了,需要淘汰一些缓存页,此时会怎样?

直接就是可以找到LRU链表中的冷数据区域的尾部的缓存页,他们肯定是之前被加载进来的,而且加载进来1s过后都没人访问过,说明这个缓存页压根儿就没人愿意去访问他,他就是冷数据。所以此时就直接淘汰冷数据区域的尾部的缓存页,刷入磁盘就可以了。

之前提到如果一个缓存页被访问了,就会把他移动到LRU链表的热数据区域首位,这么频繁的移动会导致性能不是很好。所以MySQL对LRU链表热数据区域的访问规则做了优化,只有在热数据区域的后3/4部分的缓存页被访问了,才会被移动到链表头部;链表前面1/4的缓冲页被访问,是不会被移动的。

首先,并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台线程,每隔一段时间就会把LRU链表的冷数据区域尾部的一些缓存页刷入磁盘,然后清空这几个缓存页,并把他们加入到free链表中。

所以大家会发现,只要有这个后台线程定时运行,可能你的缓存页都还没有用完呢,就给你把一批冷数据的缓存页刷入磁盘,清空出来一批缓存页,那么你就多了一批可以使用的空闲缓存页了。

如果仅仅只是把LRU链表中的冷数据区域的缓存页刷入磁盘,明显是不够的;

LRU链表中的热数据区域里的很多缓存页可能会被频繁的修改,这些数据不可能永远放在内存中,后台线程会在MySQL不繁忙的时候,把flush链表中的缓存页都刷入磁盘中,这样,被修改过的数据就被刷入到磁盘文件中了。

只要flush链表中的一些缓存页被刷入磁盘,那这些缓存页也会从flush链表和lru链表中移除,然后加入到free链表中。

所以,一边不停的加载数据到缓存页中,不停的查询和修改缓存数据,然后free链表中的缓存页不停的在减少,flush链表中的缓存页不停的在增加,lru链表中的缓存页不停的在增加和移动。

另外一边,后台线程不停的把LRU链表的冷数据区域的缓存页及flush链表的缓存页刷入到磁盘,来清空缓存页,然后flush链表和LRU链表中的缓存页不停的在减少,free链表中的缓存页在不停的增加。

如果实在没有空闲的缓存页,那就会把LRU链表冷数据区域尾部的缓存页刷入磁盘,然后清空。

1、首先明确是不是一定要上缓存,当前架构的瓶颈在哪里,若瓶颈真是数据库 *** 作上,再继续往下看。

2、明确memcached和redis的区别,到底要使用哪个。前者终究是个缓存,不可能永久保存数据(LRU机制),支持分布式,后者除了缓存的同时也支持把数据持久化到磁盘等,redis要自己去实现分布式缓存(貌似最新版本的已集成),自己去实现一致性hash。因为不知道应用场景,不好说一定要用memcache还是redis,说不定用mongodb会更好,比如在存储日志方面。

3、缓存量大但又不常变化的数据,比如评论。

4、思路是对的,清晰明了,读DB前,先读缓存,如果有直接返回,如果没有再读DB,然后写入缓存层并返回。

5、考虑是否需要主从,读写分离,考虑是否分布式部署,考虑是否后续水平伸缩。

6、想要一劳永逸,后续维护和扩展方便,那就将现有的代码架构优化,按你说的替换数据库组件需要改动大量代码,说明当前架构存在问题。可以利用现有的一些框架,比如SpringMVC,将应用层和业务层和数据库层解耦。再上缓存之前把这些做好。

7、把读取缓存等 *** 作做成服务组件,对业务层提供服务,业务层对应用层提供服务。

8、保留原始数据库组件,优化成服务组件,方便后续业务层灵活调用缓存或者是数据库。

9、不建议一次性全量上缓存,最开始不动核心业务,可以将边缘业务先换成缓存组件,一步步换至核心业务。

10、刷新内存,以memcached为例,新增,修改和删除 *** 作,一般采用lazy load的策略,即新增时只写入数据库,并不会马上更新Memcached,而是等到再次读取时才会加载到Memcached中,修改和删除 *** 作也是更新 数据库,然后将Memcached中的数据标记为失效,等待下次读取时再加载。

大方向两种方案:

1、脚本同步:自己写脚本将数据库数据写入到redis/memcached。这就涉及到实时数据变更的问题(mysql row binlog的实时分析),binlog增量订阅Alibaba 的canal ,以及缓存层数据 丢失/失效 后的数据同步恢复问题。

2、业务层实现:先读取nosql缓存层,没有数据再读取mysql层,并写入数据到nosql。nosql层做好多节点分布式(一致性hash),以及节点失效后替代方案(多层hash寻找相邻替代节点),和数据震荡恢复了。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/8530771.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-17
下一篇 2023-04-17

发表评论

登录后才能评论

评论列表(0条)

保存