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

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

相关推荐

  1. 理解和实现 LRU 缓存置换算法

    2024-04-10 17:06:02       8 阅读
  2. 缓存淘汰(LRU算法

    2024-04-10 17:06:02       11 阅读
  3. 深入理解虚拟DOM:原理优势实践

    2024-04-10 17:06:02       28 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-10 17:06:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-10 17:06:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-10 17:06:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-10 17:06:02       20 阅读

热门阅读

  1. 机器学习中的 K-均值聚类算法及其优缺点

    2024-04-10 17:06:02       21 阅读
  2. 200方啤酒酿造废水处理设备厂家定制

    2024-04-10 17:06:02       13 阅读
  3. .NET常见的20个面试题

    2024-04-10 17:06:02       14 阅读
  4. Linux 数据盘分区自动化脚本 pro/plus 版本

    2024-04-10 17:06:02       14 阅读
  5. postcss

    2024-04-10 17:06:02       16 阅读
  6. ssh远程压测断网,导致程序中断,解决方案

    2024-04-10 17:06:02       12 阅读
  7. 5.7Python之元组

    2024-04-10 17:06:02       11 阅读
  8. 释放无用的内存

    2024-04-10 17:06:02       13 阅读
  9. python实现烟花表演

    2024-04-10 17:06:02       13 阅读
  10. 队列的链表形式

    2024-04-10 17:06:02       12 阅读
  11. Rust---方法(Method)

    2024-04-10 17:06:02       13 阅读