LRU算法原理及实现

 

什么是LRU?

全名(Least recently used 最近最少使用),非常经典的一种算法,像MySQL的Buffer Pool中以及redis的删除策略使用的都是这个。

前一篇文章说过MySQL的Buffer Pool和redis都是基于内存的,且内存大小都是有限的。所以当内存空间不足时,需要淘汰一些很少使用的数据,释放内存空间给热点数据使用。

LRU是一种缓存的置换算法,一种数据淘汰策略,所以不论是缓存的内存消耗还是热度排序问题,最优的解决方案其实都是-LRU算法。那么如何确定哪些数据属于“最少使用”就很关键,大致从两个问题考虑,时间和频率。长时间没有被访问的数据可以淘汰,同时间段访问频率最少的也可以淘汰。

 

LRU是一种概念,关注的是算法实现,所以先使用一种方式来实现:

简单的链表结构:

//存放缓存的链表
public static final LinkedList<Demo> LruList = new LinkedList<>();
//链表的限制长度
public static final int CacheLimit = 3;

/**
 * 主逻辑,缓存命中,则把当前元素先从链表上删除,并且放至头部
 * @param id
 * @return
 */
public static Demo getData(String id) {
    Demo demo = null;
    synchronized (LruList) {
        for (int i = 0; i < LruList.size(); i++) {
            if (id.equals(LruList.get(i).getId())) {
                demo = LruList.remove(i);
                putNode(demo);
            }
        }
    }
    //模拟,缓存中不存在,则从DB拿数据放到缓存中
    if (null == demo){
        LruList.add(new Demo(id));
    }
    //如果链表中的热点数据超过了限制的长度大小,则删除链表尾部的数据,也就是淘汰策略的实现
    if (null != demo) {
        putNode(demo);
    }
    return null;
}


/**
 * 队列已满,删除链表尾部的数据,否则将该数据放至头部
 * @param demo
 */
public static void putNode(Demo demo){
    synchronized (LruList) {
        if (LruList.size() == CacheLimit) {
            LruList.removeLast();
        }
        LruList.addFirst(demo);
    }
}

public static void main(String[] args) {
    String[] ids = new String[]{"1", "2", "2", "4", "5", "5", "3", "5"};
    for (int i = 0; i < ids.length; i++) {
        getData(ids[i]);
    }
}

class Demo {
    private String id;

    public String getId() {
        return id;
    }

    public Demo(String id) {
        this.id = id;
    }
}

一种基于链表的LRU算法已经实现了,但仔细分析一下,是存在弊端的

  1. 性能消耗:每次命中数据后,都需要将数据重新移动至链表头部,性能消耗是很严重的。
  2. 缓存命中率:如果每次访问的数据都是在链表头部附近的,遍历会很快就定位到数据,但若数据一旦分散较为均匀,而链表又是每次都从头部遍历,访问命中率就会下降
  3. 缓存污染:并非所有的数据都有必要缓存,可能整个应用运行过程中某些数据被访问的次数很少,但是算法实现过程没法对热点数据进行区分,导致缓存中存在一些不会再被访问的数据。所以解决缓存污染的关键点就是 能识别出只访问一次或者访问次数很少的数据。

针对以上几种问题,我们来一一提出方案优化

解决方案:(HashMap + 双向链表)

1、解决性能问题

当问题无法通过算法来提升程序执行效率,并减少内存开支的时候,那就要考虑一下是不是数据结构的问题,性能的瓶颈就是在于链表的遍历,此时HashMap穿着超人内裤出现了,没错就是我!

在链表的基础上,再补充一个HashMap,key值存放数据的id,Value则存放缓存数据,key存在,那么缓存一定存在。

上面说了使用链表,每次缓存命中都会把该元素从原始位置删除,并且放至头部,当元素分散或在链表尾端,则需要全链表遍历,那么相应的替代方案就是双向链表, 链表中每一个元素都包含前后节点的引用,此时就不再需要遍历链表,调整元素的前后节点引用即可。

HashMap的底层实现原理在之前的《源码解析》- 十分钟了解HashMap的实现原理文章中有写过,不熟悉的童鞋可以移步先去了解一下。

伪代码先忽略。。。。之后补上

 

2、解决缓存命中率和缓存污染(LRU-K)

这个其实可以规避成一个问题,解决了缓存命中率实际上就是解决缓存污染的问题,

LRU-K简单理解,K表示每个缓存最近使用的次数,也就需要多维护一个队列,记录所有缓存的历史,只有当缓存被访问的次数到达一定阈值时,才将数据放入缓存。当需要淘汰时,LRU-K会淘汰第K次访问距离当前时间最大的数据。

再理的明白一点:

  1. 初问访问的数据,放至临时队列,命中次数+1
  2. 再次访问数据,命中次数继续+1
  3. 命中次数大于阈值,转入缓存队列
  4. 临时队列采用FIFO或者LRU或者时间算法实现淘汰
  5. 缓存队列可按命中的次数排序,FIFO或者时间排序实现淘汰

 


案例代码,之后会补上,此篇以理论知识为主。如果你理解了,可自行写个案例分享到评论区哈~

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值