LRU算法的详细介绍与实现

article/2025/9/29 18:55:13

1.背景

LRU(least recently used-最近最少使用算法),是一种内存数据淘汰策略,使用常见是当内存不足时,需要淘汰最近最少使用的数据。LRU常用语缓存系统的淘汰策略。

2.LRU原理

LRU最早实在操作系统接触到这个算法的,如下如所示。
在这里插入图片描述
这里的栈有别于咱们后进先出的数据结构,主要用来描述原理本身。从途中可知LRU是如何实行淘汰的,同时,大家可能也意识到这种实现可能性能并不太好,存在大量的拷贝动作。

3.LRU算法实现

我们先从一道LRU设计算法题开始。

算法题:LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。

写入数据 put(key, value) -
如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);

cache.put(2, 2);

cache.get(1); // 返回 1

cache.put(3, 3); // 该操作会使得关键字 2 作废

cache.get(2); // 返回 -1 (未找到)

cache.put(4, 4); // 该操作会使得关键字 1 作废

cache.get(1); // 返回 -1 (未找到)

cache.get(3); // 返回 3

cache.get(4); // 返回 4

分析:

如果使用数组来实现一个基于LRU的缓存,按照LRU原理要求可以预知存在大量的拷贝操作,性能上可能无法满足。设计一个LRU缓存,满足放入和移出都是O(1),我们需要把访问次序维护好,但是这个次序的维护并不需要在内存中真正排序来反应,按照这种思路,有一种实现方法就是双向链表。

API定义

class LRUCache {public LRUCache(int capacity) {}public int get(int key) {}public void save(int key, int value) {}
}

基于 HashMap + 双向链表 实现LRU

HahsMap用于快速查找到结点所在位置,然后将使用到的结点放在对头,这样最近最少使用的结点自然就落入到队尾。双向链表提供了良好的灵活性,两边可达。如下图所示。
在这里插入图片描述
假设我们需要执行如下操作,

save(“key1”, 7)

save(“key2”, 0)

save(“key3”, 1)

save(“key4”, 2)

get(“key2”)

save(“key5”, 3)

get(“key2”)

save(“key6”, 4)

使用HashMap + 双向链表数据结构实现的LRU操作双向链表部分的轨迹如下。
在这里插入图片描述

算法操作步骤如下:

  1. save(key, value):
    首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。
    如果不存在,需要构造新的节点,并且尝试把节点塞到队头。
    如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。
  2. get(key):
    通过 HashMap 找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。
    算法实现

由于可能存在并发读写LRUCache,因此需要保证线程安全。

public class LRUCache {class DLinkedNode {String key;int value;DLinkedNode pre;DLinkedNode post;}private ConcurrentMap<String, DLinkedNode> cache = new ConcurrentHashMap<String, DLinkedNode>();private int count;private int capacity;private DLinkedNode head, tail;public LRUCache(int capacity) {this.count = 0;this.capacity = capacity;head = new DLinkedNode();head.pre = null;tail = new DLinkedNode();tail.post = null;head.post = tail;tail.pre = head;}public int get(String key) {DLinkedNode node = cache.get(key);if(node == null){return -1; // should raise exception here.}moveToHead(node);return node.value;}public void put(String key, int value) {DLinkedNode node = cache.get(key);if (node != null) {node.value = value;moveToHead(node);return;      }DLinkedNode newNode = new DLinkedNode();newNode.key = key;newNode.value = value;cache.put(key, newNode);addNode(newNode);++count;if(count > capacity){// pop the tailDLinkedNode tail = popTail();cache.remove(tail.key);--count;}}private void addNode(DLinkedNode node){node.pre = head;node.post = head.post;head.post.pre = node;head.post = node;}private void removeNode(DLinkedNode node){DLinkedNode pre = node.pre;DLinkedNode post = node.post;pre.post = post;post.pre = pre;}private void moveToHead(DLinkedNode node){removeNode(node);addNode(node);}private DLinkedNode popTail(){DLinkedNode res = tail.pre;removeNode(res);return res;}
}

采用HashMap + 双向链表,提供了很好的读写操作,且能在O(1)内完成读写操作。那么,Redis的淘汰策略是不是也是根据LRU,如果是,它的淘汰算法是不是也采用的这种数据结果?

4.Redis LRU算法实现

分析Redis LRU实现之前,我们先了解一下Redis缓存淘汰策略。

当Redis内存超出物理内存限制时,内存会频繁的磁盘swap区交换数据,而交换会导致redis对外服务性能的急剧下降,这在生产环境是不允许的。说得更明白些,在生产环境是不允许交换行为的,通过设置maxmemory可限制内存超过期望大小。

当实际需要的内存大于maxmemory时,Redis提供了6种可选策略:

  1. noeviction:不继续提供写服务,读请求可以继续。
  2. volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰。也就是说没有设置过期时间的key不会被淘汰。
  3. volatile-ttl:也是淘汰设置了过期时间的key,只不过策略不是lru,而是根据剩余寿命的ttl值,ttl越小越优先被淘汰。
  4. volatile-random:同理,也是淘汰设置了过期时间的key,只不过策略是随机。
  5. allkeys-lru:类比volatile-lru,只不过未设置过期时间的key也在淘汰范围。
  6. allkeys-random:类比volatile-random,只不过未设置过期时间的key也在淘汰范围。
    采用HashMap + 双向循环链表具有较好的读写性能,但是有没有发现什么问题呢?对,HashMap和链表都存在空间浪费的情况,HashMap本来就很耗内存,双向链表由于需要空间存储指针,两种数据结构空间使用率都不高,这显然很不划算。

针对这个问题,Redis采用了近似的做法,我们来分析分析。

首先,针对问题本身,我们需要淘汰的是最近未使用的相对比较旧的数据淘汰掉,那么,我们是否一定得非常精确地淘汰掉最旧的数据还是只需要淘汰掉比较旧的数据?

咱们来看下Redis是如何实现的。Redis做法很简单:随机取若干个key,然后按照访问时间排序,淘汰掉最不经常使用的数据。为此,Redis给每个key额外增加了一个24bit长度的字段,用于保存最后一次被访问的时钟(Redis维护了一个全局LRU时钟lruclock:REDIS_LUR_BITS,时钟分辨率默认1秒)。

下面我们看下采用volatile_lru和allkeys-lru是如何实现的,关键代码如下。

// 评估object空闲时间
unsigned long estimateObjectIdleTime(robj *o) {if (server.lruclock >= o->lru) {return (server.lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;} else {return ((REDIS_LRU_CLOCK_MAX - o->lru) + server.lruclock) *REDIS_LRU_CLOCK_RESOLUTION;}
}// LRU淘汰算法实现
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{for (k = 0; k < server.maxmemory_samples; k++) {sds thiskey;long thisval;robj *o;de = dictGetRandomKey(dict);thiskey = dictGetKey(de);if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)de = dictFind(db->dict, thiskey);o = dictGetVal(de);thisval = estimateObjectIdleTime(o);/* Higher idle time is better candidate for deletion */if (bestkey == NULL || thisval > bestval) {bestkey = thiskey;bestval = thisval;}}
}

redis会基于​server.maxmemory_samples​配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高,对性能有一定影响,样本值默认为5。
在这里插入图片描述
上图是Redis官网的一组LRU统计数据,Redis3.0以上采用近视LRU算法获得了不错的效果。从Redis实现我们看出,在商业世界,为了追求空间的利用率,也会采用权衡的实现方案。

总结

LRU是缓存系统中常见的淘汰策略,当内存不足时,我们需要淘汰掉最近最少使用的数据,LRU就是实现这种策略的统称。LRU算法实现可以基于HashMap + 双向链表的数据结构实现高效数据读写,于此同时,高效查询却带来了高内存消耗的的问题,为此Redis选择了近似LRU算法,随机采用一组key,选择最老的数据进行删除,能够达到类似的效果。


http://chatgpt.dhexx.cn/article/AqUG7NsE.shtml

相关文章

LRU原来如此简单

文章目录 前言一、LRU是什么&#xff1f;二、LFU是什么&#xff1f;三、LRU和LFU的比较四、LFU代码实现&#xff08;看懂LFU就自然懂了LRU了&#xff09;1、LFU类2、Node类3、测试 写在最后&#xff0c;感谢点赞关注收藏转发 前言 现在缓存技术在项目中随处可见&#xff0c;但…

LRU算法详解

概念理解 1.LRU是Least Recently Used的缩写&#xff0c;即最近最少使用页面置换算法&#xff0c;是为虚拟页式存储管理服务的&#xff0c;是根据页面调入内存后的使用情况进行决策了。由于无法预测各页面将来的使用情况&#xff0c;只能利用“最近的过去”作为“最近的将来”的…

LRU算法

1.什么是LRU算法 LRU算法又称最近最少使用算法&#xff0c;它的基本思想是长期不被使用的数据&#xff0c;在未来被用到的几率也不大&#xff0c;所以当新的数据进来时我们可以优先把这些数据替换掉。 在LRU算法中&#xff0c;使用了一种有趣的数据结构&#xff0c;称为哈希链…

巧用 NGINX 实现大规模分布式集群的高可用性

原文作者&#xff1a;陶辉 原文链接&#xff1a;巧用 NGINX 实现大规模分布式集群的高可用性 - NGINX开源社区 转载来源&#xff1a;NGINX开源社区 本文是我对2019年GOPS深圳站演讲的文字整理。这里我希望带给各位读者的是&#xff0c;如何站在整个互联网背景下系统化地理解Ngi…

朱邦复

朱邦复 求助编辑百科名片 朱邦复&#xff0c;仓颉输入法的发明人&#xff0c;现任香港上市公司文化传信集团的副主席。湖北省黄冈县人。为中文终端机、仓颉输入法、汉卡的发明人。由于其对中文电脑发展的众多贡献&#xff0c;台湾及香港地区的华人誉其为“中文电脑之父”、“中…

TCP/IP的底层队列实现原理

个人博客请访问 http://www.x0100.top 自从上次学习了TCP/IP的拥塞控制算法后&#xff0c;我越发想要更加深入的了解TCP/IP的一些底层原理&#xff0c;搜索了很多网络上的资料&#xff0c;看到了陶辉大神关于高性能网络编程的专栏&#xff0c;收益颇多。今天就总结一下&#…

从码农到工程师:看一下这6点!

作者&#xff1a;陶辉笔记来源&#xff1a;http://www.taohui.pub 许多程序员自称码农&#xff0c;因为每天事情总也做不完&#xff0c;而这些工作也没有给自己带来职业上的提升&#xff0c;总在原地打转&#xff0c;自己的工作似乎随时可被新人替换&#xff0c;可有可无。于是…

Nginx五大类变量详解

Nginx变量详解 文章目录 Nginx变量详解一、HTTP请求相关的变量二、TCP连接相关的变量三、Nginx处理请求过程中产生的变量四、发送HTTP响应时相关的变量五、Nginx系统变量 为了方便记忆呢&#xff0c;我把nginx的全部变量分为5种&#xff0c;详情见下图 本文内容取自极客时间陶辉…

开复博士见面会

CSDN的CTO俱乐部成立一年多来&#xff0c;做过很多次活动了。我只参加了两次&#xff0c;第二次就是开复博士的创新工厂介绍会。我对创新工厂兴趣并不大&#xff0c;但很想近距离接触一下这位声名远播的演讲者和布道者。 曾经和一个&#xff36;&#xff30;的会议上&#xff…

Nginx核心知识100讲学习笔记(陶辉)Nginx架构基础(一)

(转载&#xff0c;非常不错的文章) 一、Nginx的请求处理流程进程结构 1、Nginx的请求处理流程 2、Nginx的进程结构 3、进程作用 1、Master进程 1、是进行work进程的监控管理的2、看看work进程是否正常工作需不需要进行热部署、需不需要重新载入配置文件 2、Cache manager …

在这里,NGINX 创始人 Igor Sysoev 将亲述 NGINX 的诞生史

2020 年 5 月 20 日&#xff0c;一场 NGINX 在国内的盛会、一个所有 NGINX 用户 & 爱好者朝圣的最佳场所&#xff0c;F5 线上技术峰会 – NGINX 专场将以线上直播的形式面向所有开发者召开。届时各位 NGINX 开发者心目中的偶像 NGINX 创始人 Igor Sysoev 以及国内 NGINX 技…

如何用NGINX实现UDP四层反向代理?

原文作者&#xff1a;陶辉 原文链接&#xff1a;如何用NGINX实现UDP四层反向代理&#xff1f;- NGINX开源社区 转载来源&#xff1a;NGINX开源社区 在实时性要求较高的特殊场景下&#xff0c;简单的UDP协议仍然是我们的主要手段。 UDP协议没有重传机制&#xff0c;还适用于同时…

Nginx深度剖析,真是被大牛讲透了

开篇 Nginx是一款非常出色的服务器软件&#xff0c;从开始工作到现在&#xff0c;周围所有的公司都在使用Nginx。在多年的使用过程中&#xff0c;逐渐对Nginx的源码产生了浓厚的兴趣&#xff0c;我不满足于仅仅会使用&#xff0c;我想更加深入的理解它的内部工作原理。只有深入…

CPGIS三十周年专访系列|陶闯主席

近日&#xff0c;国际华人地理信息科学协会CPGIS建会30周年之际&#xff0c;开展了一次历届主席专访活动&#xff0c;陶闯博士作为国际华人地理信息科学协会CPGIS&#xff0c;2000-2001届的主席&#xff0c;接受了CPGIS的专访&#xff0c;忆往昔&#xff0c;看今朝&#xff0c;…

【10道大厂必考的计算机网络问题】陶辉老师

目录 1.请详细介绍一下TCP的三次握手协议,为什么要三次握手&#xff1f; 2.说说HTTP协议中缓存的处理流程&#xff1f;缓存的应用流程&#xff1f;与缓存相关的HTTP头部&#xff1f; 3.在地址栏键入URL后,网络世界发生了什么&#xff1f; 4.使用HTTP长连接有哪些优点&#…

HTTP详细介绍

转自&#xff1a;HTTP详细介绍本文参考 wiki百科、陶辉老师《Web协议详解与抓包实战》 和 作者在一线多年的运维工作总结希望对大家有所帮助。常见osi模型(7层)发起组织&#xff1a; 国际电信联盟电信标准化部门&#xff0c;与国际标准组织&#xff08;ISOhttps://www.pinlue.c…

10 道大厂面试必考的计算机网络问题-陶辉 极客时间

大厂中更多会考察你的长板. 在大厂中要学会求助 1.TCP的三次握手机制,为什么要三次? 为什么需要握手? 需要同步序列号,当然也有MSS(最大报文段长度),滑动窗口. 为什么是3次? 正常想法应该是: C:我要建连接,我的seq是这个; S:我收到了 S:我的Seq是这个 C:我也收到了…

TVP思享 | 四个全新维度,极限优化HTTP性能

导语 | 当产品的用户量不断翻番时&#xff0c;需求会倒逼着你优化HTTP协议。那么&#xff0c;要想极限优化HTTP性能&#xff0c;应该从哪些维度出发呢&#xff1f;本文将由TVP陶辉老师&#xff0c;为大家分享四个全新维度。「TVP思享」专栏&#xff0c;凝结大咖思考&#xff0c…

uni-app从入门到放弃(一)

文章目录 一、uni-app简单介绍什么是uni-app&#xff1f;uni-app的优点跨平台发行&#xff0c;运行体验更好通用前端技术栈,学习成本更低开发生态,组件更丰富 二、功能框架浏览图三、创建项目1、安装HBuilderX2、创建uni-app3、运行项目4、官方提示 四、项目中使用扩展组件五、…

CUDA——从入门到放弃

1. 知识准备 1.1 中央处理器&#xff08;CPU&#xff09; 中央处理器&#xff08;CPU&#xff0c;Central Processing Unit&#xff09;是一块超大规模的集成电路&#xff0c;是一台计算机的运算核心&#xff08;Core&#xff09;和控制核心&#xff08; Control Unit&#xf…