LRU缓存淘汰算法实现方案,这次没人再说你不会开发

时间:2022-07-22
本文章向大家介绍LRU缓存淘汰算法实现方案,这次没人再说你不会开发,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

我们在平时项目开发中是不是会遇到这样的场景,每次访问的时候需要去取内存里的数据,没取到就添加到内存,但是,又不想取到的内存是过于陈旧的。那这块我们该怎么去设计算法,从而很合理的去管理我们内存的数据呢?

今天,给大家分享一个在我们开发中比较常用的缓存淘汰算法 LRU,LRU是指最近最少使用策略来管理内存数据。根据数据的历史访问记录来进行淘汰缓存,即假如数据最近被访问过,那么它以后被访问到的几率会更高,也就不会被淘汰。

01

如何实现LRU缓存淘汰算法

场景:

我们现在有这么个真实场景,我在爬取某个网站时,控制该网站的代理IP并发数,太多会搞垮对方网站的对吧,要蹲号子的呢。这里我需要维护一个代理IP代理池,而且这些IP肯定不是一直都很稳定的,但是又不能取一个就丢一个,这样太浪费资源。所以我会将这些IP缓存起来,进行按需提取,采用LRU最近最少使用的策略去管理代理IP。

实现方案

核心思想:我们维护一个有序的链表,然后在链表尾部的数据就是最早被访问过的数据,当有新的数据被访问时,就从链表的表头开始顺序遍历访问。

1,当访问的代理IP不在链表内时,就会添加到链表头部。如下,维护了一个6个节点的链表,这6个proxyIp在链表内都没有被缓存过。

2,当链表缓存满时,此时我在访问一个新的代理proxy_ip7的时候,就会就最近很少使用的表尾的proxy_ip1节点删除掉,然后,在将当前proxy_ip7添加到表头。

3,当访问的代理ip已经在缓存中时,如,proxy_ip5已经缓存过,我们将其遍历出来,然后将其所在节点删除,最后再将其插入到链表的表头。

具体链表实现方案就是这三个步骤,就能轻松实现LRU算法,下面我用java语言将其实现一遍代码的全过程,帮助大家更好的理解和使用,其他语言就根据上面实现方案是一样的哈。

对了,Redis的缓存过期的实现也是用的LRU缓存淘汰的策略,所以你看这个算法的重要性了吧

核心算法类:

public final class LRUCacheUtil {

  private LRUCacheUtil() {
  }

  /**
   * 
   * 获取指定大小的,同步的,LRU缓存Map
   * 
   * @param <K>
   * @param <V>
   * @param capacity
   * @return
   */
  public static <K, V> Map<K, V> getLRUCache(final int capacity) {
    Map<K, V> LRUMap = Collections.synchronizedMap(new LinkedHashMap<K, V>(capacity, 0.75F, true) {
      private static final long serialVersionUID = 352094108437285631L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
      }
    });
    return LRUMap;
  }

}

再写一个测试类使用下:

public class LRUCacheTest {

  private static Map<String, Long> ipFetchTimeMap = LRUCacheUtil.getLRUCache(6);
  
  public static void main(String[] args) {
    long baseTime = System.currentTimeMillis();
    ipFetchTimeMap.put("proxy_ip1", baseTime+100);
    ipFetchTimeMap.put("proxy_ip2", baseTime+200);
    ipFetchTimeMap.put("proxy_ip3", baseTime+300);
    ipFetchTimeMap.put("proxy_ip4", baseTime+400);
    ipFetchTimeMap.put("proxy_ip5", baseTime+500);
    ipFetchTimeMap.put("proxy_ip6", baseTime+600);
    
    //遍历链表缓存数据
    System.out.println("缓存中存在数据:");
    for(Map.Entry<String, Long> entry : ipFetchTimeMap.entrySet()) {
      System.out.println(entry.getKey());
    }
    
    ipFetchTimeMap.put("proxy_ip7", baseTime+600);
    
    //缓存满时,再添加新的数据
    System.out.println("缓存满时,再添加新的数据,缓存中的数据为:");
    for(Map.Entry<String, Long> entry : ipFetchTimeMap.entrySet()) {
      System.out.println(entry.getKey());
    }
    
    //访问缓存中已有的数据
    ipFetchTimeMap.get("proxy_ip5");
    System.out.println("访问缓存中已有的数据proxy_ip5,缓存数据为:");
    for(Map.Entry<String, Long> entry : ipFetchTimeMap.entrySet()) {
      System.out.println(entry.getKey());
    }
  }
}

我们再来看看运行的结果是不是和我们上面方案一样(由下往上看哈),可以回去对比方案图看看:

总结,今天我们将开发中最长遇到也是非常重要的LRU缓存淘汰算法做了详细的讲解以及具体代码实现,主要是采用维护一个链表的方案进行开发的,当然,这个方案也不是最优的,它的时间复杂度是O(n),但是这个是目前最常用的方案,我们也一直使用这个。也可以采用其他的方案,如果大家有什么好的方案,欢迎给出来更多方案,帮助我们一起学习下,毕竟,我们的初衷就是共同学深学透技术。

如果大家喜欢,或是对大家有帮助,欢迎关注我哈。

下一篇预告:数据库读写分离方面的解决方案

关于架构师修炼

本号旨在分享一线互联网各种技术架构解决方案,分布式以及高并发等相关专题,同时会将作者的学习总结进行整理并分享。

更多技术专题,敬请期待