LRU缓存实现与原理

article/2025/9/29 18:23:31

概念

  1. LRU是 Least Recently Used 的缩写,即最近最少使用页面置换算法,是为虚拟页式存储管理服务的,是根据页面调入内存后的使用情况进行决策了。由于无法预测各页面将来的使用情况,只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU算法就是将最近最久未使用的页面予以淘汰。

  2. 操作系统里,在内存不够的场景下,淘汰旧内容的策略。LRU … Least Recent Used,淘汰掉最不经常使用的。因为计算机体系结构中,最大的最可靠的存储是硬盘,容量很大,并且内容可以固化,但是访问速度很慢,所以需要把使用的内容载入内存中;内存速度很快,但是容量有限,并且断电后内容会丢失,并且为了进一步提升性能,还有CPU内部的 L1 Cache,L2 Cache 等概念。因为速度越快的地方,它的单位成本越高,容量越小,新的内容不断被载入,旧的内容肯定要被淘汰,所以就有这样的使用背景。

原理

最近最少使用算法,核心思想是:最近使用的数据很大概率将会再次被使用。而最近一段时间都没有使用的数据,很大概率不会再使用。做法:把最长时间未被访问的数据置换出去。这种算法是完全从最近使用的时间角度去考虑的。
在这里插入图片描述
执行过程理解:

  1. 在缓存中查找客户端需要访问的数据 如果缓存命中,则将访问的数据中队列中取出,重新加入到缓存队列的头部。
  2. 如果没有命中,表示缓存穿透,将需要访问的数据从磁盘中取出,加入到缓存队列的尾部;
  3. 如果此时缓存满了,则需要先置换出去一个数据,淘汰队列尾部的数据,然后再在队列头部加入新数据。

存在的问题:

  • 缓存污染:如果某个客户端访问大量历史数据时,可能使缓存中的数据被这些历史数据替换,其他客户端访问数据的命中率大大降低。

题目

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

示例:

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 总体上是这样的,最近使用的放在前边(最左边),最近没用的放到后边(最右边),来了一个新的数,如果内存满了,把旧的数淘汰掉,那位了方便移动数据,我们肯定不能考虑用数组,呼之欲出,就是使用链表了,解决方案:链表(处理新老关系)+ 哈希(查询在不在),分析如下:

  1. 底层应该用链表,按照数据的新旧程度来排列,旧的在左边,新的在右边,新来一个加到尾部(你可以想象自己从左往右画一条链表),删除是删头,除了这两个操作,还有就是把一个数据从中间拿出来放尾巴上(这个数组就很难做到)
  2. 这里还有一个需求,就是要知道这个数据有没有存在于链表中,如果不在链表中,加到尾巴即可,如果已经在链表中,就只要更细数据的位置,如何查找这个数据在不在呢,这就用哈希表。
  3. 考虑删除操作,要把当前节点的前一个节点的指针的改变,获取它前一个节点,方便的数据结构就是 双向链表

所以我们用的数据结构就是 LinkedList (底层是双向链表)+ HashMap,也直接用 LinkedHashMap 更为方便。看面试官要求是啥了。

ps:其实也可以用单链表,只要在 map 中不存当前节点,而是存当前节点的前驱即可。

算法实现

算法一:LinkedHashMap

class LRUCache {int capacity;Map<Integer, Integer> map;public LRUCache(int capacity) {this.capacity = capacity;map = new LinkedHashMap<>();}public int get(int key) {if (!map.containsKey(key)){return -1;}// 先删除旧的位置,再放入新位置Integer value = map.remove(key);map.put(key, value);return value;}public void put(int key, int value) {if (map.containsKey(key)){map.remove(key);map.put(key, value);return;}map.put(key, value);// 超出capacity,删除最久没用的,利用迭代器删除第一个if(map.size() > capacity){map.remove(map.entrySet().iterator().next().getKey());}}
}/*** Your LRUCache object will be instantiated and called as such:* LRUCache obj = new LRUCache(capacity);* int param_1 = obj.get(key);* obj.put(key,value);*/

算法二:双链表+HashMap

class LRUCache {//定义双向链表节点public class ListNode{int key;int value;ListNode pre;ListNode next;public ListNode(int key, int value){this.key = key;this.value = value;pre = null;next = null;}}private int capacity;private Map<Integer, ListNode> map;private ListNode head;private ListNode tail;public LRUCache(int capacity) {this.capacity = capacity;map = new HashMap<>();head = new ListNode(-1, -1);tail = new ListNode(-1, -1);head.next = tail;tail.pre = head;}public int get(int key) {if(!map.containsKey(key)) {return -1;}ListNode node = map.get(key);// 先删除该节点,再接到尾部node.pre.next = node.next;node.next.pre = node.pre;moveToTail(node);return node.value;}public void put(int key, int value) {// 直接调用这边的get方法,如果存在,它会在get内部被移动到尾巴,不用再移动一遍,直接修改值即可if(get(key) != -1) {map.get(key).value = value;return;}// 若不存在,new一个出来,如果超出容量,把头去掉ListNode node = new ListNode(key, value);map.put(key, node);moveToTail(node);if(map.size() > capacity) {map.remove(head.next.key);head.next = head.next.next;head.next.pre = head;}}// 把节点移动到尾巴private void moveToTail(ListNode node) {node.pre = tail.pre;tail.pre = node;node.pre.next = node;node.next = tail;}
}/*** Your LRUCache object will be instantiated and called as such:* LRUCache obj = new LRUCache(capacity);* int param_1 = obj.get(key);* obj.put(key,value);*/

算法三:单链表

class LRUCache {// 定义单链表节点private class ListNode{int key;int value;ListNode next;public ListNode(int key, int value){this.key = key;this.value = value;this.next = null;}}private int capacity;private Map<Integer, ListNode> map;private ListNode head;private ListNode tail;public LRUCache(int capacity) {this.capacity = capacity;map = new HashMap<>();head = new ListNode(-1, -1);tail = head;}public int get(int key) {if(!map.containsKey(key)){return -1;}// map中存放的是要找的节点的前驱ListNode pre = map.get(key);ListNode cur = pre.next;// 把当前节点删掉并移到尾部if(cur != tail){pre.next = cur.next;// 更新它后面 node 的前驱map.put(cur.next.key, pre);map.put(cur.key, tail);moveToTail(cur);}return cur.value;}public void put(int key, int value) {if(get(key) != -1){map.get(key).next.value = value;return;}// 若不存在则 new 一个ListNode node = new ListNode(key, value);// 当前 node 的 pre 是 tailmap.put(key, tail);moveToTail(node);if(map.size() > capacity){map.remove(head.next.key);map.put(head.next.next.key, head);head.next = head.next.next;}}public void moveToTail(ListNode node){node.next = null;tail.next = node;tail = tail.next;}
}

LRU 在 MySQL 中的应用

MySQL 中的 Buffer Pool 也是用来加速查询的缓存,当 Buffer Pool的容量被占满时,也需要淘汰数据,其中数据的淘汰也是基于LRU算法的。

所有从磁盘上读取的数据首先都会缓存在 Buffer Pool中。当对存放大量冷数据的表进行查询时,会在短时间内将大量冷数据加载到 Buffer Pool 中,如果 Buffer Pool 被占满之后就会根据 LRU 算法淘汰数据,可能就把之间的热点数据淘汰了,从而导致的缓存命中率下降。

为了避免因为冷数据表的查询导致热点数据被淘汰的问题,MySQL 对 LRU 算法进行了改进,将 Buffer Pool 分成 young 和 old 两个区域,所有数据被加载进 Buffer Pool 时都是先放在 old 区,当在 old 区待足够长的时间或被访问次数达到阈值时数据才会被放到 young 区。数据淘汰也是优先从 old 区淘汰。这样就能避免大量冷数据加载导致的热点数据淘汰问题。

LRU 在 Redis 中的应用

Redis 中淘汰数据有两个方式:

  • 定期删除:Redis 有后台线程,会定时扫描数据,从中选择应该淘汰的过期数据将其删除。假如每次删除都对所有的 key 进行一次最近访问时间排序的话,对性能消耗非常大,Redis 采用的是随机抽样的方式进行删除,例如 LRU 算法删除,则随机抽取 20 个 key,从中找出最近未访问的 key 进行删除。这样随机抽取能提高性能,但是不能覆盖到所有的 key,会存在一个问题,对于设计了过期时间的数据,理论上来说应该将其删除,但是多次随机抽取均为选中,因此未被淘汰。
  • 惰性删除:针对定期删除不能完全淘汰所有应该淘汰的数据的问题,当 Redis 访问了 key 之后,Redis 会判断 key 是否已经过期,如果已经过期就直接删掉。

Redis 的6种内存淘汰策略,设置参数: maxmemory-policy noeviction, 内存设置参数: maxmemory

  1. noenviction:默认策略,写请求失败,读请求正常,del 请求正常;
  2. volatile-lru:在设置了过期键的键空间中移除最近最少使用的 key;
  3. volatile-random:在设置过过期键的键空间中随机移除部分 key;
  4. volatile-ttl:在设置过期键的键空间中挑选将要过期的数据淘汰;
  5. allkeys-lru:在所有键空间中移除最近最少使用的可以;
  6. allkeys-random:在所有键空间中随机移除部分随机移除部分 key;

实际应用

  LRU 算法也可以用于一些实际的应用中,如你要做一个浏览器,或类似于淘宝客户端的应用的就要用到这个原理。大家都知道浏览器在浏览网页的时候会把下载的图片临时保存在本机的一个文件夹里,下次再访问时就会,直接从本机临时文件夹里读取。但保存图片的临时文件夹是有一定容量限制的,如果你浏览的网页太多,就会一些你最不常使用的图像删除掉,只保留最近最久使用的一些图片。这时就可以用到 LRU 算法 了,这时上面算法里的这个特殊的栈就不是保存页面的序号了,而是每个图片的序号或大小;所以上面这个栈的元素都用 Object 类来表示,这样的话这个栈就可以保存的对象了。

漫画图解

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
用户信息当然是存在数据库里。但是由于我们对用户系统的性能要求比较高,显然不能每一次请求都去查询数据库。所以,在内存中创建了一个哈希表作为缓存,每次查找一个用户的时候先在哈希表中查询,以此提高访问性能。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
让我们以用户信息的需求为例,来演示一下 LRU 算法的基本思路:

  1. 假设我们使用哈希链表来缓存用户信息,目前缓存了 4个用户,这 4 个用户是按照时间顺序依次从链表右端插入的。
    在这里插入图片描述

  2. 业务方访问用户 5,由于哈希链表中没有用户 5 的数据,我们从数据库中读取出来,插入到缓存当中。这时候,链表中最右端是最新访问到的用户 5,最左端是最近最少访问的用户 1。
    在这里插入图片描述

  3. 业务方访问用户 2,哈希链表中存在用户 2 的数据,我们怎么做呢?我们把用户 2 从它的前驱节点和后继节点之间移除,重新插入到链表最右端。这时候,链表中最右端变成了最新访问到的用户 2,最左端仍然是最近最少访问的用户 1。
    在这里插入图片描述
    在这里插入图片描述

  4. 业务方请求修改用户 4 的信息。同样道理,我们把用户 4 从原来的位置移动到链表最右侧,并把用户信息的值更新。这时候,链表中最右端是最新访问到的用户 4,最左端仍然是最近最少访问的用户 1。
    在这里插入图片描述
    在这里插入图片描述

5.后来业务方换口味了,访问用户 6,用户 6 在缓存里没有,需要插入到哈希链表。假设这时候缓存容量已经达到上限,必须先删除最近最少访问的数据,那么位于哈希链表最左端的用户1就会被删除掉,然后再把用户 6 插入到最右端。
在这里插入图片描述
在这里插入图片描述

参考链接

LRU算法思想及手写LRU实现
全面讲解LRU算法
漫画:什么是LRU算法?


http://chatgpt.dhexx.cn/article/9GqZKHRL.shtml

相关文章

LRU算法的详细介绍与实现

1.背景 LRU&#xff08;least recently used-最近最少使用算法&#xff09;&#xff0c;是一种内存数据淘汰策略&#xff0c;使用常见是当内存不足时&#xff0c;需要淘汰最近最少使用的数据。LRU常用语缓存系统的淘汰策略。 2.LRU原理 LRU最早实在操作系统接触到这个算法的…

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、官方提示 四、项目中使用扩展组件五、…