LRU算法中那些你不知道的事

本文探讨了LRU算法在计算机内存管理中的应用,如MySQL的BufferPool和Redis的内存淘汰策略。重点讲解了如何解决MySQL中的预读失效和BufferPool污染问题,以及Redis对传统LRU的优化,包括近似LRU实现和LFU淘汰策略的引入。最后介绍了LRU缓存实战,以LeetCode题目为例,展示了LinkedHashMap在LRU缓存中的关键作用。
摘要由CSDN通过智能技术生成

计算机中的LRU算法

LRU(Least Recently Used,最近最少使用算法)

  • 传统的LRU算法
    • 用链表管理数据;当访问的数据在链表中,把链表中的数据移动到表头/尾;当访问的数据不在链表中,从磁盘中读取数据,把数据放到链表头/尾,同时还要淘汰LRU链表末尾的节点。
    • 后面会给出Java实现的方法,使用LinkedHashMap可以很好地实现我们的需求;
  • MySQL中的LRU:InnoDB存储引擎中的Buffer Pool,使用LRU算法管理缓存页。
    • buffer pool是InnoDB设计的一个缓冲池,以页为单位缓存磁盘数据。从而提高读写效率。
  • Redis中的LRU:Redis中的内存淘汰策略中,有LRU算法,将最近最少使用的key淘汰出内存。
    • 需要注意的是,Redis实现的是近似LRU算法,并不是我们想的链表方式;
  • 操作系统中的LRU:页面置换算法中可以使用LRU页面置换算法,将最近最少使用的页面置换出内存。
    • 当我们试图在通过虚拟内存地址访问一个页表时,该页表不在内存中,就会发生缺页异常,需要将页从磁盘换入到内存中,但是如果内存满,需要将内存中的页面置换出来,此时就需要页面置换算法。

MySQL对LRU的修改

如果使用传统的LRU算法,MySQL中可能出现预读失效Buffer Pool污染的问题。

  • 预读失效:MySQL 在加载数据页时,会提前把它相邻的数据页一并加载进来,目的是为了减少磁盘 IO。但是可能这些被提前加载进来的数据页,并没有被访问,相当于这个预读是白做了,这个就是预读失效。
    • 解决预读失效(提前淘汰预读页),可以将LRU划分为young区域和old区域。用户直接读取的页面,直接移动到young区域的头部;预读到的页放到old区域的头部中,当真正被访问到时,才放入young区域的头部。这样才不会让我们的预读白做,即使预读页没被访问,也会很快被删除。·
  • Buffer Pool污染:当某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。
    • 解决Buffer Pool污染。
      • 设置LRU链表的插入点,也就是设置old区域所占的百分比。新扫描到的数据插入到old区域,防止替换掉young区域的热数据。innodb_old_blocks_pct参数控制该百分比,默认值为 37,即young / old = 63/37。
      • 并且提高进入到 young 区域的门槛,增加一个old区域停留的时间判断,只有满足被访问 + 大于该停留时间,才会将数据页放在young区域头部。这个间隔时间是由 innodb_old_blocks_time 控制的,默认是 1000 ms。
  • 另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。

Redis对LRU的修改

如果使用传统的LRU算法,Redis不想维护一个较大的链表,占用内存空间。

Redis实现的是近似LRU算法,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。

当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

Redis的LRU算法存在缓存污染的问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

所以Redis4.0之后新增了lfu淘汰策略。

LRU缓存实战

LeetCode : 146.LRU 缓存
在这里插入图片描述
LRU缓存算法的核心数据结构:LinkedHashMap。
LinkedHashMap本质还是一个HashMap,但是维护了一个双向链表。不同于HashMap,LinkedHashMap可以维护map的插入顺序,得到插入的第一个元素:

// 删除第一个元素
LinkedHashMap map = new LinkedHashMap();
int key = map.keySet().iterator().next();

// 获取某个元素
LinkedHashMap map = new LinkedHashMap();
int value = map.get(key);

注意,在这个LRU链表中,最常访问的元素我们放在了链表末尾,当元素满时,每次移除链表头的元素。

class LRUCache {
    LinkedHashMap<Integer,Integer> cache = new LinkedHashMap<>();
    int capacity;
    public LRUCache(int capacity) {
        this.capacity = capacity;
    }
    
    public int get(int key) {
        // 如果缓存中本来没有该元素
        if(!cache.containsKey(key)){
            return -1;
        }
        // 返回的答案
        int val = cache.get(key);
        // 因为访问了该元素,移出缓存
        cache.remove(key);
        //放进缓存,此时就在链表末尾,代表最近最多使用
        cache.put(key,val);
        return val;
    }
    
    public void put(int key, int value) {
        if(cache.containsKey(key)){
            cache.remove(key);
        }
        cache.put(key,value);
        if(cache.size() > capacity){
             // 移出队头元素 使用 cache.keySet().iterator().next();
            int delKey = cache.keySet().iterator().next();
           
            cache.remove(delKey);
        }
    }
}

重点

文章原创不易,点个赞支持下~

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sivan_Xin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值