我们在平时项目开发中是不是会遇到这样的场景,每次访问的时候需要去取内存里的数据,没取到就添加到内存,但是,又不想取到的内存是过于陈旧的。那这块我们该怎么去设计算法,从而很合理的去管理我们内存的数据呢?
今天,给大家分享一个在我们开发中比较常用的缓存淘汰算法 LRU,LRU是指最近最少使用策略来管理内存数据。根据数据的历史访问记录来进行淘汰缓存,即假如数据最近被访问过,那么它以后被访问到的几率会更高,也就不会被淘汰。
如何实现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),但是这个是目前最常用的方案,我们也一直使用这个。也可以采用其他的方案,如果大家有什么好的方案,欢迎给出来更多方案,帮助我们一起学习下,毕竟,我们的初衷就是共同学深学透技术。
如果大家喜欢,或是对大家有帮助,欢迎关注我哈。
下一篇预告:数据库读写分离方面的解决方案
在公众号“架构师修炼”可获得专属java架构视频资料,更多java、python、人工智能、小程序、大前端等可看菜单,无私奉献,你会感谢我的
往期精选