概念

RU是Least Recently Used 的缩写,翻译过来就是“最近最少使用”。

LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

方法选择

  1. 用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。

  2. 利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。

  3. 利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

对于第一种方法, 需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。所以在一般使用第三种方式来是实现LRU算法。

为什么这里要用双链表呢,单链表为什么不行?

链表删除元素除了自己本身的指针信息,还需要前驱节点的指针。要求时间复杂度是O(1)的情况下,双链表才能直接获取到前驱节点的指针。

哈希表里面已经保存了 key ,那么链表中为什么还要存储 key 和 value 呢,只存入 value 不就行了?

删除了链表中的节点时,哈希表也得对应删除。删除哈希表需要key,这个key值要来源于链表中的结点。

实现方案(基于LinkedHashMap)

LinkedHashMap关注度

关注点 结论
LinkedHashMap是否允许空 Key和Value都允许空
LinkedHashMap是否允许重复数据 Key重复会覆盖、Value允许重复
LinkedHashMap是否有序 有序
LinkedHashMap是否线程安全 非线程安全

LinkedHashMap底层就是用的HashMap加双链表实现的,而且本身已经实现了按照访问顺序的存储。此外,LinkedHashMap中本身就实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数,方法默认是直接返回false,不会移除元素,所以需要重写该方法。即当缓存满后就移除最不常用的数。

java中最简单的LRU算法实现,就是利用jdk的LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可。

public class LRU<K,V> {

  private static final float hashLoadFactory = 0.75f;
private LinkedHashMap<K,V> map;
private int cacheSize; public LRU(int cacheSize) {
this.cacheSize = cacheSize;
int capacity = (int)Math.ceil(cacheSize / hashLoadFactory) + 1;
// 指定容量、装载因子和键值对保持顺序,True访问顺序,False为插入顺序
map = new LinkedHashMap<K,V>(capacity, hashLoadFactory, true){
private static final long serialVersionUID = 1; @Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > LRU.this.cacheSize;
}
};
} public synchronized V get(K key) {
return map.get(key);
} public synchronized void put(K key, V value) {
map.put(key, value);
} public synchronized void clear() {
map.clear();
} public synchronized int usedSize() {
return map.size();
} public void print() {
for (Map.Entry<K, V> entry : map.entrySet()) {
System.out.print(entry.getValue() + "--");
}
System.out.println();
}
}

更多实现内容参考:https://www.cnblogs.com/lzrabbit/p/3734850.html

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

改进方案

1.LRU-K

LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。

LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。

数据第一次被访问时,加入到历史访问列表,如果书籍在访问历史列表中没有达到K次访问,则按照一定的规则(FIFO,LRU)淘汰;当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列中删除,将数据移到缓存队列中,并缓存数据,缓存队列重新按照时间排序;缓存数据队列中被再次访问后,重新排序,需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即“淘汰倒数K次访问离现在最久的数据”。

LRU-K具有LRU的优点,同时还能避免LRU的缺点,实际应用中LRU-2是综合最优的选择。由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多。

2.Two queue

Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。

当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。

新访问的数据插入到FIFO队列中,如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;如果数据在FIFO队列中再次被访问到,则将数据移到LRU队列头部,如果数据在LRU队列中再次被访问,则将数据移动LRU队列头部,LRU队列淘汰末尾的数据。

3.Multi Queue(MQ)

MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据。 详细的算法结构图如下,Q0,Q1....Qk代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数的队列:

新插入的数据放入Q0,每个队列按照LRU进行管理,当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列中删除,加入到高一级队列的头部;为了防止高优先级数据永远不会被淘汰,当数据在指定的时间里没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;需要淘汰数据时,从最低一级队列开始按照LRU淘汰,每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部。如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列头部。 Q-history按照LRU淘汰数据的索引。

MQ需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。

改进方案的代码实现,参考:

https://blog.csdn.net/Scape1989/article/details/78006267

https://blog.51cto.com/9250070/2540073

LRU类算法对比

由于不同的访问模型导致命中率变化较大,此处对比仅基于理论定性分析,不做定量分析。

对比点 对比
命中率 LRU-2 > MQ(2) > 2Q > LRU
复杂度 LRU-2 > MQ(2) > 2Q > LRU
代价 LRU-2 > MQ(2) > 2Q > LRU

实际应用中需要根据业务的需求和对数据的访问情况进行选择,并不是命中率越高越好。

例如:虽然LRU看起来命中率会低一些,且存在”缓存污染“的问题,但由于其简单和代价小,实际应用中反而应用更多。

LRU 在 Redis 中的应用

既然是内存淘汰算法,那么我们常用的 Redis 里面必然也有对应的实现。

Redis 的内存淘汰策略有如下几种:

  1. noenviction:默认策略。不继续执行写请求(DEL 请求可以处理),读请求可以继续进行。
  2. volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。没有设置过期时间的 key 不会被淘汰。
  3. volatile-random:从已设置过期时间的数据集中随机选择数据淘汰。
  4. volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
  5. allkeys-lru:和 volatile-lru 不同的是,这个策略要淘汰的 key 对象是全体的 key 集合。
  6. allkeys-random:从所有数据集中随机选择数据淘汰。

在 Redis 中的 LRU 算法本来就不是严格的 LRU 算法,原因LRU会消耗更多的内存,Redis 3.0 对 LRU 算法进行了进一步改进,增加了淘汰池。

问题:数据库中有 3000w 的数据,而 Redis 中只有 100w 数据,如何保证 Redis 中存放的都是热点数据?

先指定淘汰策略为 allkeys-lru 或者 volatile-lru,然后再计算一下 100w 数据大概占用多少内存,根据算出来的内存,限定 Redis 占用的内存。接下来的,就交给 Redis 的淘汰策略了。

127.0.0.1:6380> config set maxmemory 50m //设置redis最大使用50M内存
OK
127.0.0.1:6380> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:6380> config set maxmemory-policy allkeys-lru//设置lru算法为allkeys-lru
OK

[整理] LRU 算法的实现方式的更多相关文章

  1. java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现

    java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析 ...

  2. FJUTOJ-3682 LRU算法的实现2 (链表+哈希)

    传送门 此题让我们实现一个LRU的模板类.本题较简便且高效的写法是维护一个std::list和一个std::unordered_map. std::list 与 std::unordered_map ...

  3. linkedhashmap中关于LRU算法的实现

    //LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面 public LinkedHashMap(int ...

  4. 万字长文,以代码的思想去详细讲解yolov3算法的实现原理和训练过程,Visdrone数据集实战训练

    以代码的思想去详细讲解yolov3算法的实现原理和训练过程,并教使用visdrone2019数据集和自己制作数据集两种方式去训练自己的pytorch搭建的yolov3模型,吐血整理万字长文,纯属干货 ...

  5. SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)

    在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...

  6. 基于思岚A1激光雷达+OpenGL+VS2017的Ramer-Douglas-Peucker算法的实现

    时隔两年 又借到了之前的那个激光雷达,最老版本的思岚A1,甚至不支持新的固件,并且转接板也不见了,看了下淘宝店卖¥80,但是官方提供了一个基于STM32的实现方式,于是我估摸着这个转接板只是一个普通的 ...

  7. 图像数据到网格数据-2——改进的SMC算法的实现

    概要 本篇接上一篇继续介绍网格生成算法,同时不少内容继承自上篇.上篇介绍了经典的三维图像网格生成算法MarchingCubes,并且基于其思想和三角形表实现了对样例数据的网格构建.本篇继续探讨网格生成 ...

  8. Bug2算法的实现(RobotBASIC环境中仿真)

    移动机器人智能的一个重要标志就是自主导航,而实现机器人自主导航有个基本要求--避障.之前简单介绍过Bug避障算法,但仅仅了解大致理论而不亲自动手实现一遍很难有深刻的印象,只能说似懂非懂.我不是天才,不 ...

  9. Canny边缘检测算法的实现

    图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波.我们知道微分运算是求信号的变化率,具有加强高频分量的作用.在空域运算中来说,对图像的锐化就是计算微分.由于数字图像的离散信号, ...

随机推荐

  1. leetcode 864. 获取所有钥匙的最短路径(BFS,状态压缩)

    题目链接 864. 获取所有钥匙的最短路径 题意 给定起点,要求在最短步骤内收集完所有钥匙,遇到每把锁之前只有 有对应的钥匙才能够打开 思路 BFS+状态压缩典型题目 先确定起点和总的钥匙数目,其次难 ...

  2. Python利用最优化算法求解投资内部收益率IRR【一】

    一. 内部收益率和净现值 内部收益率(Internal Rate of Return, IRR)其实要和净现值(Net Present Value, NPV)结合起来讲.净现值指的是某个投资项目给公司 ...

  3. oracle 12C单实例打PSU

    前提: oracle不管打什么样的补丁,readme都是很好的参考资料. Oracle每季度都会更新一个最新的PSU,现在12.1.0.2.0的最新的PSU是Patch 26925311. 由于今天白 ...

  4. DOI技术扫盲一

    DOI:  desktop office intergration   桌面办公软件集成简单的将,就是我们在Windows桌面中打开的办公软件(如:word,excel,pdf等等)可以在SAP系统进 ...

  5. Maven 知识点总结以及解决jar报冲突的几种方法

    1.常见的命令 Compile Test Package Install Deploy Clean 2.坐标的书写规范 groupId 公司或组织域名的倒序 artifactId 项目名或模块名 ve ...

  6. 学习es6构造函数的第一天

    什么是面向对象 编程思维分为,面向过程和面向对象 面向过程就像一个人,一间屋子,一个床 一个人走进了屋子,上了床 二面向对象 人,屋子,床 可以是屋子里进了一个人,上了床 或者,屋子里的床上有一个人 ...

  7. (012)每日SQL学习:TO_CHAR(DATE,FORMAT)

    SYSDATE 2009-6-16 15:25:10 TRUNC(SYSDATE) 2009-6-16 TO_CHAR(SYSDATE,'YYYYMMDD') 20090616 到日 TO_CHAR( ...

  8. Cognos软件介绍文档(原创)

    1. Cognos简介 Cognos是世界上最大的业务智能软件制造商,它能够帮助用户提取公司数据,然后分析并汇总得出报告.Cognos有许多产品,但最为著名的还是它的PowerPlay联机分析处理(o ...

  9. Maven环境搭建以及在IDEA中的配置与简单入门

    目录 一.下载与安装 二.配置 1. 环境变量 2. 阿里云镜像 3. 本地仓库 三.IDEA创建Maven项目 1. 创建一个原始的Maven项目 1.2 指定模板创建(可选) 2. 配置GAV 3 ...

  10. SpringMVC听课笔记(九:数据转换 & 数据格式化 & 数据校验)

    1.数据绑定流程 --1). Spring MVC主框架将ServletRequest对象及目标方法入参实例传递给WebDataBinderFactory实例,以创建DataBinder实例对象. - ...