LRU算法的详细介绍与实现

2023-11-06

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 tail
            DLinkedNode 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,选择最老的数据进行删除,能够达到类似的效果。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LRU算法的详细介绍与实现 的相关文章

随机推荐

  • dll调用nodejs的回调函数

    nodejs使用ffi调用dll dll中有回调函数调用js中的方法 c语言中cdll h文件 extern C typedef void JsCall int index 这个就是要传入的类型结构 extern declspec dlli
  • 监控项目里的流媒体服务器,监控项目里的流媒体服务器

    监控项目里的流媒体服务器 内容精选 换一换 共享型和独享型负载均衡算法 支持以下几种调度算法 加权轮询算法 根据后端服务器的权重 按顺序依次将请求分发给不同的服务器 它用相应的权重表示服务器的处理性能 按照权重的高低以及轮询方式将请求分配给
  • QT设置ToolButton按钮的样式

    QToolButton min width 80px min height 32px QToolButton color rgb 255 255 255 min height 20 border style solid border top
  • 巴比特

    摘要 据 科创板日报 7 月 11 日报道 北京市经济和信息化局党组书记 局长姜广智在接受记者采访时表示 北京经信局将在算力供给层面提升中长期算力供给能力 加快建设海淀区北京人工智能公共算力 朝阳区北京数字经济算力中心等重点项目 尽快形成算
  • Pandas-连接合并函数merge()

    一 merge函数用途 pandas中的merge 函数类似于SQL中join的用法 可以将不同数据集依照某些字段 属性 进行合并操作 得到一个新的数据集 二 merge 函数的具体参数 用法 DataFrame1 merge DataFr
  • C++_面向对象_1

    设计一个圆形类 Circle 和一个点类 Point 计算点和圆的关系 class Circle public int x int y int radius class Point public int x int y void judge
  • SS626V100 SDK安装编译osdrv问题汇总

    文章目录 前言 1 开发环境 2 在 linux 服务器上安装交叉工具链 2 1 安装 aarch64 mix410 linux tgz 2 2 安装 cc riscv32 cfg11 musl 20211008 elf tar gz 2
  • react,umi,antd-pro的layout封装过程

    import React from react import Layout Form Icon from antd import isEqual from lodash isEqual 深度比较对象 import memoizeOne fr
  • TIOBE 8 月编程语言:C、Java 差距拉大,R 语言盛行

    编程语言社区 TIOBE 最新发布了 8 月编程语言排行榜 相比上个月 本月 TIOBE 指数整个体变化并不大 C 语言依然保持强劲地增长势头 与第二名 Java 之间差距逐月增大 从上个月相差 1 35 的份额逐步增长到 2 55 的差额
  • 数据分析学习之路——(八)分类算法介绍

    前面几篇文章都是从数据分析介绍讲到描述统计分析 其实数据分析还需要使用机器学习的相关知识用来建立不同的分析模型 最终对数据信息进行深入的分析和挖掘 在实际工作当中 我们需要对数据进行特征分析 并且从数据中获取有价值的信息 并且为数据产品的市
  • 时不我待,拥抱趋势,开源IM项目OpenIM技术简介

    坚持开源 开源的理念是基于共享 合作和透明的原则 将软件 代码等知识资源公开并允许他人使用 修改和重新分发 以促进创新和发展 以下是几个开源的优点 创新 开源可以促进创新 通过让其他人改进或扩展已有的代码或项目 不断推动技术的进步 透明 开
  • C# TCP/IP网络数据传输及实现

    C TCP IP网络数据传输及实现 一 概念简述 1 什么是OSI 和TCP IP 2 什么是套接字Socket 3 TCP 和 UDP 4 IP MAC PORT 1 IP地址 2 MAC地址 3 Port端口号 二 UDP上位机的实现
  • 静态集合类

    如HashMap LinkedList等等 如果这些容器为静态的 那么它们的生命周期与程序一致 则容器中的对象在程序结束之前将不能被释放 从而造成内存泄漏 生命周期长的对象持有短生命周期对象的引用 尽管短生命周期的对象不再使用 但是因为长生
  • 服务器上部署scrapy爬虫项目

    爬爬们 如果你已经开始部署项目了 那么你肯定也已经写好了完整的爬虫项目 恭喜你 你很优秀 今天忙了小半天的服务器部署 跟大家分享一些心得 首先我们要有一台服务器 不好意思 这是废话 略过 安装python 下载安装包 好习惯可以自己创建文件
  • Acwing 897. 最长公共子序列

    f i j 表示所有在第一个序列的前i个字母中出现 且在第二个序列的前j个字母中出现的子序列中的最大个数 include
  • canvas在图片上做标记,可以单一也可以多个

  • docker 安装mongo数据库

    1 pull镜像 docker pull mongo 4 2 创建目录 mkdir p mongodb datadb chmod 777 mongodb datadb 3 运行 准备好目录之后 就可以开始运行 Docker 镜像了 dock
  • AI,v3,百度人脸识别库上传---node

    config有必要的grant type client id client secret var https require https var request require request var qs require querystr
  • The mbstring extension is missing. Please check your PHP configuration.

    在安装完毕wamp程序后 启动后访问phpmyadmin 出现错误 The mbstring extension is missing Please check your PHP configuration 解决方案 在php ini中修改
  • LRU算法的详细介绍与实现

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