Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何动手撸一个LRU缓存

如何动手撸一个LRU缓存

作者头像
我是攻城师
发布于 2019-07-31 03:30:14
发布于 2019-07-31 03:30:14
66200
代码可运行
举报
文章被收录于专栏:我是攻城师我是攻城师
运行总次数:0
代码可运行

上篇文章介绍了,如何动手实现一个LFU缓存,今天我们来学习下如何动手实现一个LRU缓存,在这之前,我们还是先回顾下关于缓存置换算法的三种策略:

我们知道缓存置换算法主流的有三种,分别是:

(1) FIFO:First In First Out,先进先出策略

(2) LFU:Least Frequently Used,最不经常使用策略

(3) LRU:Least Recently Used,最近最少使用策略

LRU 全称 Least Recently Used,基于数据访问历史记录来执行淘汰策略,LRU是首先淘汰最长时间未被使用的页面,这种算法把近期最久没有被访问过的页面作为被替换的页面,与LFU不一样的是,LRU并不关注缓存数据的访问次数,它只关注该条数据的访问时间。核心思想:如果一个数据在最近一段时间内没被访问,则在将来一段时间内被访问的可能性也很小。

LRU缓存的实现相比LFU来说要简单一点,最关键的在于LRU缓存插入,查询和删除可以做到O(1)的时间复杂度,这一点相比LFU的O(logN)来说在大数据量下LRU表现更好,此外LRU非常适合存在热点数据的场景。

我们看下实现的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LruCache {
    //设置的虚拟头节点
    Node head = new Node(0, 0);
    //设置的虚拟尾部节点
    Node tail = new Node(0, 0);

    //使用map来提升查询性能
    Map<Integer, Node> map = new HashMap<>();
    int capacity;//缓存保存数据的大小

    public LruCache(int capacity) {
        this.capacity = capacity;
        //初始化
        head.next = tail;
        tail.prev = head;
    }

    //查询数据
    public int get(int key) {
        //判断是否存在该条数据
        if (map.containsKey(key)) {
            Node node = map.get(key);
            remove(node);
            insert(node);
            //如果存在,就直接返回,并把该条数据从原来的位置移除,放入链表的头部
            return node.value;
        } else {
            //不存在就返回-1
            return -1;
        }
    }


    public void put(int key, int value) {
        //如果该节点已经存在,就删除该节点
        if (map.containsKey(key)) {
            remove(map.get(key));
        }
        //判断缓存容量是否达到上限
        if (map.size() == capacity) {
            remove(tail.prev);//如果达到上限就移除链表最后的节点
        }
        //插入新节点
        insert(new Node(key, value));
    }

    //移除无效的node
    public void remove(Node node) {
        map.remove(node.key);//移除目标节点
        //建立双向链表关系
        //目标节点的前置指向目标节点的后置
        node.prev.next = node.next;
        //目标节点的后置指向目标节点的前置
        node.next.prev = node.prev;
    }

    //插入一个节点
    private void insert(Node node) {
        //将数据,放入map中,加速查询
        map.put(node.key, node);
        //将新插入的数据,放在链表的头部,这里使用头插法,性能O(1)
        Node headNext = head.next;
        head.next = node;
        node.prev = head;
        headNext.prev = node;
        node.next = headNext;
    }


    //定义一个双向链表
    class Node {
        Node prev;
        Node next;
        int key;
        int value;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

}

代码非常简单,这里简单分析下原理。

首先让我们分析下LRU缓存为什么能达到O(1)的时间复杂度。众所周知,双向链表的插入和删除可以达到O(1)的时间复杂度,但双向链表的缺点在于,其查询的时间复杂度为O(n)这就是它的短板,那么如何加速查询?这里我们可以利用HashMap来当索引查询,从而使得双向链表的查询的时间复杂度也能达到O(1),没错LRU的实现原理,就是充分结合了两种数据结构的优点而衍生出来的。我们看下面的一张图就非常直观的显示了这种关系:

上图中HashMap的key就是链表数据的key,而value就是该Node在双向链表里面的位置,也就是指针地址。

明白了原理之后,我们在看代码逻辑就一目了然了,简单分析下:

(1)首先我们定义一个双向链表的结构:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//定义一个双向链表
    class Node {
        Node prev;
        Node next;
        int key;
        int value;

        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

更通用的策略可以把key和value做成泛型

(2)在LRUcache类里面,我们定义了虚拟的head和tail节点其目的是为了方便操作删除动作,此外我们还定义了一个缓存容量和辅助的map结构来加速查询。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//设置的虚拟头节点
    Node head = new Node(0, 0);
    //设置的虚拟尾部节点
    Node tail = new Node(0, 0);

    //使用map来提升查询性能
    Map<Integer, Node> map = new HashMap<>();
    int capacity;//缓存保存数据的大小

    public LruCache(int capacity) {
        this.capacity = capacity;
        //初始化
        head.next = tail;
        tail.prev = head;
    }

(3)put方法分析

put方法中,我们首先判断要插入的key是否存在,如果存在就删除掉,方便将其移动到链表头部位置,接着我们判断缓存的容量是否超出指定大小,如果超出就要淘汰最旧的数据,也就是位于链表尾部(尾部是虚拟的)的节点的前一个节点。最后执行插入方法。

(4)insert方法分析

insert方法非常简单,首先将要插入的节点,给加入到map里面建立索引,然后接着使用头插法,将节点插入链表的头部。

(5)remove方法分析

remove时候,首先要删除HashMap里面的索引数据,这样新的查询就不会查询到该条数据,接着删除自身,删除自身的方法非常简单,就是讲自身的前置节点的引用指向自身的next节点,然后将自身next节点的prev指针指向自身的prev节点。这样就完成 了删除操作,而自身由于没有对象再引用自己,所以在下次GC时可以回收掉这部分资源。

(6)get方法分析

get方法就查询方法,直接判断map里面是否存在该条数据,如果存在就返回,并把访问的节点移到链表的头部,代表最近访问过。如果不存在就返回-1代表查询的节点不存在。

可以看到实现一个LRU缓存并不是很难,如果大家对Java里面的LinkedHashMap比较熟悉的话,就会发现LinkedHashMap的原理就与我们上面分析的实现非常相似,这也是为什么LinkedHashMap本身也可以用来做LRU缓存的原因。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>(16,0.75f,true);

友情提示:代码块可左右滑动

LinkedHashMap的第三个参数,就是用来控制开启LRU功能的关键:

true代表LinkedHashMap使用访问顺序,这就是LRU缓存的功能

false代表LinkedHashMap使用插入顺序,这就维持自然的插入顺序。

本文主要结合代码介绍了LRU缓存的原理和实现,LRU缓存的应用场景主要在于互联网项目的热点数据访问,但对偶发性的、周期性的批量操作会导致LRU算法命中率急剧下降,从而降低其性能,这一点我们应该了解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
想进大厂、想提升底层功力、想搞懂LRU缓存,这一篇就够了!
历史题目:Ceph MDS 路径解析场景: 输入:/mnt/data/project1/report.docx 输出:逐级路径 ["mnt", "data", "project1", "report.docx"]
早起的鸟儿有虫吃
2025/07/10
1840
想进大厂、想提升底层功力、想搞懂LRU缓存,这一篇就够了!
146. LRU 缓存机制
要在O(1)时间复杂度完成这两种操作,我们想到的使用HashMap来进行操作,而且参考LRUCache的特性,需要对元素进行移动或者删除,首选的是双向链表。
用户7447819
2021/07/23
3060
手写LRU算法
LRU是redis的缓存过期淘汰策略(Least Recently Used),最近最少使用的一种算法,选择最久未使用的数据将其淘汰。 redis缓存的淘汰策略有很多:
gzq大数据
2021/06/09
4440
LC146— LRU 缓存机制
146. LRU 缓存机制 难度中等1299 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。 实现 LRUCache 类: LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存
Java架构师必看
2021/05/14
3830
Leetcode LRU 缓存机制
缓存是一种提高数据读取性能的技术,在计算机中cpu和主内存之间读取数据存在差异,CPU和主内存之间有CPU缓存,而且在内存和硬盘有内存缓存。当主存容量远大于CPU缓存,或磁盘容量远大于主存时,哪些数据应该被应该被清理,哪些数据应该被保留,这就需要缓存淘汰策略来决定。常见的策略有三种:先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。
用户10384376
2023/02/25
3390
Leetcode LRU 缓存机制
golang刷leetcode 经典(1) LRU缓存机制
设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作:获取数据 get 和 写入数据 put 。
golangLeetcode
2022/08/02
4480
聊聊缓存淘汰算法-LRU 实现原理
我们常用缓存提升数据查询速度,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来。缓存数据不能随机删除,一般情况下我们需要根据某种算法删除缓存数据。常用淘汰算法有 LRU,LFU,FIFO,这篇文章我们聊聊 LRU 算法。
andyxh
2019/10/30
8570
聊聊缓存淘汰算法-LRU 实现原理
LRU缓存
LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解,本文就带你写一手漂亮的代码。
狼啸风云
2023/11/18
2500
Redies 淘汰策略的 LRU 算法你知道吗?
最近有个小伙伴跟我诉苦,说他面试的时候被问到Redis的淘汰策略,这个问题他是有准备的
阿凯
2022/01/07
5790
Redies 淘汰策略的 LRU 算法你知道吗?
手写LRU缓存淘汰算法
在我们这个日益追求高效的世界,我们对任何事情的等待都显得十分的浮躁,网页页面刷新不出来,好烦,电脑打开运行程序慢,又是好烦!那怎么办,技术的产生不就是我们所服务么,今天我们就聊一聊缓存这个技术,并使用我们熟知的数据结构--用链表实现LRU缓存淘汰算法。
Simon郎
2021/03/01
7980
LeetCode-146-LRU缓存机制
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
benym
2022/07/14
3260
Redis:内存被我用完了!该怎么办?
Redis是一个内存数据库,当Redis使用的内存超过物理内存的限制后,内存数据会和磁盘产生频繁的交换,交换会导致Redis性能急剧下降。所以在生产环境中我们通过配置参数maxmemoey来限制使用的内存大小。
Java识堂
2021/02/05
4890
Redis:内存被我用完了!该怎么办?
LRU缓存淘汰机制C++实现
LRU 是 Least Recently Used 的简写,字面意思是最近最少使用。
evenleo
2020/10/13
8490
算法题就像搭乐高:手把手带你拆解 LRU 算法
LRU 算法就是一种缓存淘汰策略,原理不难,但是面试中写出没有 bug 的算法比较有技巧,需要对数据结构进行层层抽象和拆解,本文 labuladong 就给你写一手漂亮的代码。
labuladong
2021/09/23
6060
LRU算法与Caffeine、Redis中的缓存淘汰策略
在现代计算机系统中,缓存是提高系统性能的关键技术之一。为了避免频繁的IO操作,常见的做法是将数据存储在内存中的缓存中,以便快速访问。然而,由于内存资源有限,缓存的大小是有限的,因此需要一种策略来淘汰缓存中的数据,以便为新的数据腾出空间。本文将介绍一种常用的缓存淘汰策略——最近最少使用(Least Recently Used,LRU)算法,并且比较它与Caffeine和Redis中的缓存淘汰策略。
疯狂的KK
2023/08/17
5600
LRU算法与Caffeine、Redis中的缓存淘汰策略
☆打卡算法☆LeetCode 146. LRU 缓存 算法解析
请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类:
恬静的小魔龙
2022/08/07
3370
☆打卡算法☆LeetCode 146. LRU 缓存 算法解析
谈一谈缓存
“ 在计算机世界里,缓存可以说无处不在,无论是硬件,还是软件,缓存都是一种最使用的优化手段,可以在操作系统读取磁盘数据时、也可以在应用访问数据库数据时,还可以是本地程序访问网络数据时……”
搬砖俱乐部
2020/01/17
4520
谈一谈缓存
昨天面试被问到的 缓存淘汰算法FIFO、LRU、LFU及Java实现
第一次请求时把计算好的结果存放在缓存中,下次遇到同样的请求时,把之前保存在缓存中的数据直接拿来使用。
万猫学社
2022/04/22
3580
实现一个LRU真的好难呐
不知道屏幕前的朋友们,有没有和我一样,觉得LRU算法原理很容易理解,实现起来却很复杂。
虎妞先生
2023/05/01
6230
LRU 缓存机制实现:哈希表 + 双向链表
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
一个会写诗的程序员
2021/03/23
2.1K0
LRU 缓存机制实现:哈希表 + 双向链表
相关推荐
想进大厂、想提升底层功力、想搞懂LRU缓存,这一篇就够了!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验