[整理] LRU 算法的实现方式
概念
RU是Least Recently Used 的缩写,翻译过来就是“最近最少使用”。
LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
方法选择
用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。
利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。
利用链表和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 的内存淘汰策略有如下几种:
- noenviction:默认策略。不继续执行写请求(DEL 请求可以处理),读请求可以继续进行。
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。没有设置过期时间的 key 不会被淘汰。
- volatile-random:从已设置过期时间的数据集中随机选择数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- allkeys-lru:和 volatile-lru 不同的是,这个策略要淘汰的 key 对象是全体的 key 集合。
- 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 算法的实现方式的更多相关文章
- java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现
java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析 ...
- FJUTOJ-3682 LRU算法的实现2 (链表+哈希)
传送门 此题让我们实现一个LRU的模板类.本题较简便且高效的写法是维护一个std::list和一个std::unordered_map. std::list 与 std::unordered_map ...
- linkedhashmap中关于LRU算法的实现
//LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面 public LinkedHashMap(int ...
- 万字长文,以代码的思想去详细讲解yolov3算法的实现原理和训练过程,Visdrone数据集实战训练
以代码的思想去详细讲解yolov3算法的实现原理和训练过程,并教使用visdrone2019数据集和自己制作数据集两种方式去训练自己的pytorch搭建的yolov3模型,吐血整理万字长文,纯属干货 ...
- SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)
在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...
- 基于思岚A1激光雷达+OpenGL+VS2017的Ramer-Douglas-Peucker算法的实现
时隔两年 又借到了之前的那个激光雷达,最老版本的思岚A1,甚至不支持新的固件,并且转接板也不见了,看了下淘宝店卖¥80,但是官方提供了一个基于STM32的实现方式,于是我估摸着这个转接板只是一个普通的 ...
- 图像数据到网格数据-2——改进的SMC算法的实现
概要 本篇接上一篇继续介绍网格生成算法,同时不少内容继承自上篇.上篇介绍了经典的三维图像网格生成算法MarchingCubes,并且基于其思想和三角形表实现了对样例数据的网格构建.本篇继续探讨网格生成 ...
- Bug2算法的实现(RobotBASIC环境中仿真)
移动机器人智能的一个重要标志就是自主导航,而实现机器人自主导航有个基本要求--避障.之前简单介绍过Bug避障算法,但仅仅了解大致理论而不亲自动手实现一遍很难有深刻的印象,只能说似懂非懂.我不是天才,不 ...
- Canny边缘检测算法的实现
图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波.我们知道微分运算是求信号的变化率,具有加强高频分量的作用.在空域运算中来说,对图像的锐化就是计算微分.由于数字图像的离散信号, ...
随机推荐
- kubernets之secret资源
一 对于一些保密度比较高的文件,k8s又是如何存储的呢? 针对那些保密度比较高的配置文件,例如证书以及一些认证配置不能直接存储在configmap中,而是需要存储在另外一种资源中,需要对存储在里面的 ...
- P1967 货车运输(倍增LCA,生成树)
题目链接: https://www.luogu.org/problemnew/show/P1967 题目描述 A国有n座城市,编号从 1到n,城市之间有 m 条双向道路.每一条道路对车辆都有重量限制, ...
- MyBatis初级实战之四:druid多数据源
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Junit测试和反射
Junit单元测试 测试分类 黑盒测试:不需要写代码,给输入值,看程序能否得到输出期望值. 白盒测试:需要些代码,关注程序具体的执行流程. Junit的使用 步骤 定义一个测试类(测试用例). 定义测 ...
- linux下安装zsh和p10k的详细过程
目录 下载zsh 下载oh-my-zsh 切换shell 下载p10k 下载zsh sudo apt-get install zsh sudo apt-get install git 下载oh-my- ...
- Kubernetes集群管理工具kubectl命令技巧大全
一. kubectl概述 Kubectl是用于控制Kubernetes集群的命令行工具,通过kubectl能够对集群本身进行管理,并能够在集群上进行容器化应用的安装部署. kubectl命令的语法如下 ...
- TCP服务器程序
Linux下编写TCP服务器调用的函数顺序为:socket -> bind -> listen -> accept -> recv/send socket 参见:http:// ...
- RVA与FOA的转换
主要取决于文件对齐与内存对齐的值
- mysqldump 内存消耗
MySQL :: MySQL 8.0 Reference Manual :: 4.5.4 mysqldump - A Database Backup Program https://dev.mysql ...
- 【练习】goroutine chan 通道 总结
1. fatal error: all goroutines are asleep - deadlock! 所有的协程都休眠了 - 死锁! package mainimport("fmt&q ...