经典算法之LRU算法

https://www.bilibili.com/video/BV1Z24y1k7cY


一、理论

LRU算法算是个常见的算法,很有必要去了解它,现在我们就来看看什么是 LRU

LRU 的全称是 Least Recently Used(最近最少使用),就如它的含义一样,最近最少使用的。在实际的场景中大多会把它当作一种 淘汰策略

它的应用场景也很简单,我们的存储空间总是有限的,一旦超过了最大限度就必须要淘汰一些数据,那淘汰哪种数据才算是合理的呢?合理的方式有很多,不同的场景也不一样,但淘汰 最近最少使用的数据,在绝大部分场景下都是合理的,所以LRU算法很常见。



二、实践


3-1、Redis 淘汰策略

redis的6大淘汰策略里面就有2种是 LRU策略

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错

3-2、MyBtais 二级缓存

关于MyBatis的缓存可以参看这篇文章 https://blog.csdn.net/Tomwildboar/article/details/127570765

MyBatis的二级缓存各种功能实现是基于 装饰器模式来实现的,默认就是 LRU

下面是MyBatis 的 LruCache源码, 因为比较少就全部粘贴了

package org.apache.ibatis.cache.decorators;

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.ibatis.cache.Cache;

/**
 * Lru (least recently used) cache decorator.
 *
 * @author Clinton Begin
 */
public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); // touch
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }
}

它里面主要有两个操作

  1. 对 LinkedHashMap 的 removeEldestEntry 方法进行了重写
  2. 添加的时候会去调用 cycleKeyList 方法

removeEldestEntry

LinkedHashMap 底层也还是基于HashMap的,并且没有重写 put方法,是直接调用的 HashMap的put方法

在HashMap的put方法里面最后会去调用 afterNodeInsertion 方法, LinkedHashMap 里面重写了这个方法如下, evict 默认就是 true

这个方法的描述 possibly remove eldest ,可能移除最老的(也就是 first),当if判断是ture的时候就会删除这个元素

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

我们再来看看重写后的 removeEldestEntry ,eldest 通过上面其实就是链表头第一个元素

keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
    private static final long serialVersionUID = 4267176411845948333L;

    @Override
    protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
    boolean tooBig = size() > size;
        if (tooBig) {
            eldestKey = eldest.getKey();
        }
        return tooBig;
    }
};

如果当前容器里面的数据大于规定的容量,就执行 eldestKey = eldest.getKey();


cycleKeyList
添加元素的时候会调用这个方法

private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
        delegate.removeObject(eldestKey);
        eldestKey = null;
    }
}

上面我们知道,当元素大于容量规定大小,eldestKey 就会等于第一个元素的key,这时候就会删掉这个元素,从而实现 LRU策略


3-3、MySQL bufferPool

上面这种LRU策略,在某种情况下会存在漏洞,如果我们每次都是淘汰第一个元素,那如果我们在查询到元素A后就一直不再使用它了,这样它从链表尾到链表尾才会被淘汰,但是我们有一个元素B在它的后面但是经常被使用,但元素B依旧比元素A先淘汰,这样就不合理了。

MySQL 操作数据的时候先会把数据加载到内存中(Buffer Pool),但是内存的大小是有限的,所以也就存在了淘汰策略,如果只是简单的使用LRU策略,就会存在上述的问题。

MySQL对LRU策略进行了改造来完善这个问题,完整的buffer pool 参看这里 https://blog.csdn.net/Tomwildboar/article/details/121525187

它的逻辑也很简单

  1. 把内存分为 冷数据区和热数据区,数据默认都是进入冷数据区
  2. 冷数据区的数据经过默认的时间间隔被访问就会进入热数据区(默认 1s)
  3. 热数据区也进行划分,前 1/4的热数据区访问的时候不会进行移动
  4. 后 3/4的热数据被访问的时候会被提到前 1/4的位置
  5. 淘汰的时候优先淘汰冷数据区

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值