深入理解LRU缓存算法:原理、应用与优化

news2024/11/26 16:57:18

LRU算法(Least Recently Used,最近最少使用算法)的思想是基于"时间局部性"原理,即在一段时间内,被访问过的数据在未来仍然会被频繁访问的概率较高。

LRU 原理

LRU算法的主要思想是将最近被使用的数据保留在缓存中,而最久未被使用的数据则被替换出去。它维护一个缓存空间,当需要替换数据时,选择缓存中最久未被使用的数据进行替换。

具体实现时,LRU算法通常使用一种数据结构,比如双向链表(Doubly Linked List)和哈希表(Hash Table)的组合来实现。每个节点在双向链表中保存了数据的值,并且通过哈希表提供了快速的数据查找能力。

在这里插入图片描述

LRU 局部性场景

尽管LRU算法在许多情况下表现良好,但在某些特定情况下可能无法很好地适应,包括以下几种情况:

  1. 突发访问模式(Bursty Access Pattern)如果访问模式发生突变,例如某个数据在一段时间内被频繁访问,然后突然不再被访问,LRU算法可能无法及时将其替换出缓存。这是因为LRU算法仅根据最近使用的时间进行替换决策,而不考虑访问频率的变化。

  2. 热点数据(Hotspot Data)当存在少数数据被频繁访问,而其他数据很少被访问时,LRU算法可能无法很好地区分热点数据和冷门数据。即使某个数据被频繁访问,但如果它在缓存中的位置靠后,LRU算法可能会将其替换出去,从而导致频繁访问的数据被频繁地加载到缓存中,影响性能。

  3. 数据访问分布不均匀(Skewed Data Access Pattern)如果数据的访问分布不均匀,即部分数据被频繁访问而其他数据很少被访问,LRU算法可能无法很好地利用缓存空间。因为LRU算法只关注最近访问的数据,而不管数据的访问频率。这可能导致一些常用数据无法保持在缓存中,而被替换出去。

在这些情况下,可以考虑其他缓存替换算法,如LFU(Least Frequently Used,最不经常使用算法)或者根据具体需求选择其他算法的变种,以更好地适应实际的数据访问模式。

LRU 实现

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的
  • 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置

首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在O(1)的时间内完成 get或者 put 操作。具体的方法如下:

  • 对于get操作,首先判断 key 是否存在:.

    • 如果 key 不存在,则返回 -1;
    • 如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
  • 对于put操作,首先判断 key 是否存在:

    • 如果 key不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
    • 如果 key 存在,则与get操作类似,先通过哈希表定位,再将对应的节点的值更新头value ,并将该节点移到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和[在双向链表的头部添加节点」两步操作,都可以在O(1)时间内完成。

在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,以避免对头尾指针额外的操作

public class LRUCache {
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }

    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }
}

InnoDB LRU 原理

InnoDB将LRU链表分为两个部分,也就是所谓的old区young区

  • young区在链表的头部,存放经常被访问的数据页,可以理解为热数据
  • old区在链表的尾部,存放不经常被访问的数据页,可以理解为冷数据

这两个部分的交汇处称为midpoint,分区比例可以使用以下参数设置

show variables like 'innodb_old_blocks_pct';

InnoDB LRU 链表

数据页第一次被加载进Buffer Pool时在old区的头部。当这个数据页在old区,再次被访问到,会做如下的判断:如果这个数据页在LRU链表中的old区 存在的时间超过了1秒,就把它移动到young区

时间设置参数为innodb_old_blocks_time

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1582484.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【小程序】常用方法、知识点汇总1

欢迎来到《小5讲堂》 这是《小程序》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 前言请求超时Markdown解析逐行显示效果文本变动事件转发…

Vue-Router入门

现在的前后端分离项目&#xff0c;后端只管数据传递&#xff0c;视图跳转的活交由前端来干了&#xff0c;vue-router就是专门来干这个活的&#xff0c;它可以让页面跳转到指定组件 组件是可复用的 Vue 实例, 把一些公共的模块抽取出来&#xff0c;然后写成单独的的工具组件或者…

Live800:理解、调节与管理客户情绪,提升客户满意度

在企业与客户的交互过程中&#xff0c;客户情绪的管理是至关重要的。一个成功的企业不仅要提供优质的产品或服务&#xff0c;还需要关注和理解客户的情绪&#xff0c;有效地调节和管理客户的情绪&#xff0c;以提升客户满意度。文章从三个方面进行深入探讨&#xff1a;理解客户…

Netty的基本架构与组件

Netty实战精髓 前言 Netty的组成部分 1、Channel 2、Callback 3、Future ChannelFuture 提供多个附件方法来允许一个或者多个 ChannelFutureListener 实例&#xff0c;这个回调方法 operationComplete() 会在操作完成时调用。 4、Event和Handler 5、EventLOOP Netty 通过触发…

Mysql底层原理十:Redo log

3.7 Redo log Redo log记录的是物理日志&#xff0c;具体就是哪个表空间&#xff0c;哪个数据页&#xff0c;哪个偏移量&#xff0c;改了几个字节&#xff0c;改成什么表空间号数据页号偏移量修改几个字节的值具体的值 3.7.1 Redo block &#xff08;批处理缓存&#xff09;…

基于SSM+Jsp+Mysql的物流管理系统

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

44.网络游戏逆向分析与漏洞攻防-角色管理功能通信分析-角色创建服务器反馈数据包分析

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果 现在的代码都是依据数据包来写的&#xff0c;如果看不懂代码&#xff0c;就说明没看懂数据包…

程序员必须要知道的一个在线专业书网站

网站&#xff1a; https://awesome-programming-books.github.io/ https://git-scm.com/book/zh/v2 截图如下&#xff0c;可以看到&#xff0c;里面有很多数&#xff0c;可以在线看&#xff0c;免得去到处找了。

TCP挥手中TIME_WAIT存在的原因

四次挥手的一般过程如图所示&#xff1a; 在客户端收到FIN结束报文的时候不是立刻进入CLOSED状态&#xff0c;而是进入TIME_WAIT状态&#xff0c;一般等2MLS后进入关闭状态。 原因&#xff1a; 1.可靠地终止 TCP 连接。 2.保证让迟来的 TCP报文段有足够的时间被识别并丢弃。 …

求三角形面积(C语言)

一、运行结果&#xff1b; 二、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h> # include <math.h>int main() {//初始化变量值&#xff1b;double a, b, c, s, area;//赋值&#xff1b;a 3.67;b 5.43;c 6.21;//运算求s&#xff1b…

【Linux系统】进程状态

1.直接谈论Linux的进程状态 Linux进程状态本质上是task_struct这个结构体内的一个变量用来存储进程状态。 task_struct { //内部的一个属性 int status; } R运行状态&#xff08;running&#xff09;: 并不意味着进程一定在运行中&#xff0c;它表明进程要么是在运行中要么在运…

月子会所ERP管理云平台 StarryQuoteEdit.aspx SQL注入漏洞复现

0x01 产品简介 月子会所ERP管理云平台是武汉金同方科技有限公司专为为母婴服务行业提供信息化解决方案,是结合行业顶级月子中心相关企业需求开发的一套综合性管理软件。该系统全面管控月子中心经营过程中的各个环节,提高总店及分店月子中心管理水平,规范月子中心从业人员操作…

达梦的归档日志参数ARCH_RESERVE_TIME测试

达梦的参数ARCH_RESERVE_TIME测试 前面有提到和oracle相比&#xff0c;达梦的归档日志相关参数有个比较特别&#xff0c;可以通过设置它去规定归档日志的保留时间。 ARCH_RESERVE_TIME&#xff1a;归档日志保留时间&#xff0c;单位分钟&#xff0c;取值范围 0~2147483647。只…

SQL 注入之 Windows/Docker 环境 SQLi-labs 靶场搭建!

在安全测试领域&#xff0c;SQL注入是一种常见的攻击方式&#xff0c;通过应用程序的输入执行恶意SQL查询&#xff0c;从而绕过认证和授权&#xff0c;可以窃取、篡改或破坏数据库中的数据。作为安全测试学习者&#xff0c;如果你要练习SQL注入&#xff0c;在未授权情况下直接去…

GmSSL-3.1.1编译

1.源码下载&#xff1a; 下载地址&#xff1a;https://github.com/guanzhi/GmSSL/releases选择对应版本下载。 ​ 2.选择要下载的源码包&#xff1a; ​ 2.编译&#xff1a; 2.1 windows编译&#xff1a;打开vs命令行&#xff0c;选择想要编译的版本&#xff0c;x86或x64…

leetcode73 矩阵置零

题目描述 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用原地算法。 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]] 输入&#xff1a;matrix [[0,1,2,0],[3,4…

电脑远程控制esp32上的LED

1、思路整理 首先esp32需要连接上wifi 然后创建udp socket 接受udp数据 最后解析数据&#xff0c;控制LED 2、micropython代码实现 import network from socket import * from machine import Pin p2Pin(2,Pin.OUT)def do_connect(): #连接wifi wlan network.WLAN(network.…

linux fixmap分析

本文基于Linux-4.19.125&#xff0c; ARM V7&#xff0c;dual core, MMU采用2级页表&#xff08;未开启LPAE&#xff09;。 1 为什么需要fixmap Linux内核启动过程中&#xff0c;经过汇编阶段后&#xff0c;mmu功能已经开启&#xff0c;后续只能通过虚拟地址来访问DDR&#x…

如何通过VPN访问内网?

VPN&#xff08;Virtual Private Network&#xff09;是一种通过公共网络建立私有网络连接的技术&#xff0c;可以在不同地点的网络中建立安全通道&#xff0c;实现远程访问内网资源的目的。本文将介绍如何通过VPN访问内网&#xff0c;并介绍一款名为“天联”的VPN服务。 什么是…

solidity(3)

地址类型 pragma solidity ^0.8.0;contract AddressExample {// 地址address public _address 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;address payable public _address1 payable(_address); // payable address&#xff0c;可以转账、查余额// 地址类型的成员uint256…