当前位置: 首页 > article >正文

08模拟法 + 技巧 + 数学 + 缓存(D4_缓存)

目录

讲解一:基础学习

一、基本介绍

二、衡量指标

三、关于LRU 和 LFU 的应用

-------------------------------------------

讲解二:四种常见缓存算法

一、LRU

1. 原理

2. 手写必要性

3. 市场调查

4. 代码实现

5. 手写心得

6. 知识小结

二、LFU

1. 原理

2. 手写必要性

3. 市场调研

4. 代码实现

5. 知识小结

三、RR

四、FIFO

-------------------------------------------

参考文献


讲解一:基础学习

一、基本介绍

缓存算法是编程中的基本算法,用于解决缓存的更新和替换问题,通过合理地选择缓存中数据的存

储位置,可以提高系统的访问速度和性能。本文介绍几个通用的缓存算法,这些算法适用于多种应

用场景的缓存策略,其目标是在限定的缓存空间内,最大化缓存命中率,同时最小化缓存淘汰率。

  1. 随机替换 (Random Replacement,RR):随机选择一个数据项淘汰。
  2. 先进先出(First In First Out, FIFO):根据数据项进入缓存的时间先后,淘汰最早进入缓存的数据项。
  3. 最近最少使用(Least Recently Used, LRU):根据数据项最近被访问的时间,淘汰最久未被使用的数据项。
  4. 最少使用(Least Frequently Used, LFU):根据数据项被访问的频率,淘汰访问次数最少的数据项。

二、衡量指标

衡量一个缓存算法的质量,通常看以下指标:

  1. 命中率(Hit Rate):即缓存中已缓存的数据被访问的次数与所有访问次数的比值,反映了缓存算法对于热点数据的缓存效果。
  2. 缓存空间利用率(Cache Space Utilization):即缓存中已经占用的空间与总空间的比值,反映了缓存算法对于缓存空间的利用效率。
  3. 替换次数(Replacement Count):即缓存中数据被替换的次数,反映了缓存算法对于缓存数据的保护能力。
  4. 缓存访问速度(Cache Access Speed):即缓存中数据被访问的速度,反映了缓存算法对于访问速度的提升效果。

不过值得注意的是,不同应用场景和需求会对缓存算法的指标有不同的要求,比如某些场景可能更

注重命中率和访问速度,而另一些场景则可能更注重缓存空间利用率和替换次数。因此,在选择缓

存算法时,需要根据实际情况进行权衡和选择。

三、关于LRU 和 LFU 的应用

根据具体的应用场景和缓存需求,如果数据的使用频率比较均匀,没有明显的热点数据,那么 

LRU 算法比较适合。

例如,一个在线书店的图书搜索页面,用户搜索图书的请求会比较频繁,但是对于每本书的访问并

没有特别的频繁,这时 LRU 算法就能够很好地满足需求。

如果数据有明显的热点,即某些数据被频繁访问,而其他数据则很少被访问,那么 LFU 算法比较

适合。

例如,一个视频网站的首页,某些热门视频会被很多用户频繁地访问,而其他视频则很少被访问,

这时 LFU 算法就能够更好地满足需求。

这些算法有一些实际的应用例子:

  1. 操作系统中的页面置换算法:在虚拟内存中,操作系统需要根据页面的访问情况进行置换,常用的算法包括 LRU 和 LFU。
  2. Web 服务器中的缓存算法:对于一些静态内容,如图片、CSS 文件等,Web 服务器可以使用 LRU 或 LFU 算法进行缓存,以提高响应速度和并发能力。
  3. 数据库中的缓存算法:数据库可以使用 LRU 或 LFU 算法来缓存一些常用的数据块,以减少磁盘 I/O 操作,提高访问速度。
  4. 编程语言中的垃圾回收算法:编程语言需要对内存进行垃圾回收,常用的算法包括 LRU 和 LFU。其中 LRU 算法被用来确定哪些对象是最近使用过的,而 LFU 算法被用来确定哪些对象是最频繁使用的。

 

-------------------------------------------

讲解二:四种常见缓存算法

一、LRU

1. 原理

LRU(Least Recently Used)缓存算法是一种常用的缓存淘汰策略,它的基本思想是根据数据的

访问时间来进行淘汰,最近使用的数据被认为是将来也可能会被使用的,因此被保留,而较久未使

用的数据被认为是将来可能不会再使用的,因此被淘汰。

2. 手写必要性

手写LRU缓存算法的必要性在于深入理解算法的实现原理和核心思想,同时可以根据实际需求进行

优化和定制化,提高缓存的效率和命中率。

3. 市场调查

根据市场调查,LRU缓存算法在各个领域都有广泛的应用,特别适用于需要频繁读取和更新数据的

场景,如数据库查询、网络请求、页面缓存等。同时,随着大数据和云计算的发展,对高效缓存算

法的需求也越来越大。

4. 代码实现

该实现使用了一个双向链表和一个HashMap来保存缓存数据,其中双向链表用于维护缓存数据的

访问顺序。在访问缓存数据时,通过将访问到的节点移动到双向链表的头部来表示该节点最近被访

问过;而在添加新的缓存数据时,如果缓存已经满了,则会先移除双向链表尾部的节点。

import java.util.HashMap;
import java.util.Map;

public class LRUCache<K, V> {
    
    private final Map<K, Node> cache;
    private final int capacity;
    private Node head;
    private Node tail;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>(capacity);
    }

    public V get(K key) {
        Node node = cache.get(key);
        if (node == null) {
            return null;
        }
        moveToHead(node);
        return node.value;
    }

    public void put(K key, V value) {
        Node node = cache.get(key);
        if (node == null) {
            node = new Node(key, value);
            cache.put(key, node);
            addNode(node);
            if (cache.size() > capacity) {
                Node removed = removeTail();
                cache.remove(removed.key);
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }

    private void moveToHead(Node node) {
        if (node == head) {
            return;
        }
        removeNode(node);
        addNode(node);
    }

    private void addNode(Node node) {
        if (head == null) {
            head = node;
            tail = node;
        } else {
            node.next = head;
            head.prev = node;
            head = node;
        }
    }

    private void removeNode(Node node) {
        if (node == head) {
            head = node.next;
        } else if (node == tail) {
            tail = node.prev;
        } else {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
        node.next = null;
        node.prev = null;
    }

    private Node removeTail() {
        Node removed = tail;
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            tail = tail.prev;
            tail.next = null;
        }
        return removed;
    }

    private class Node {
        private final K key;
        private V value;
        private Node prev;
        private Node next;

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

5. 手写心得

通过手写LRU缓存算法,我们深入理解了其实现原理和核心思想。

LRU缓存算法可以提高缓存的效率和命中率,适用于各种需要频繁读取和更新数据的场景。

在实际应用中,我们可以根据具体需求进行优化和定制化,进一步提升算法的性能。

6. 知识小结

这种算法是根据数据项的历史访问记录来选择替换掉最近最少被使用的数据项。其核心思想是:如

果一个数据在最近一段时间内没有被访问,那么它在未来被访问的概率也相对较低,可以考虑将其

替换出缓存,以便为后续可能访问的数据腾出缓存空间。

LRU算法有多种实现方式,其中一种比较简单的实现方式是使用双向链表和哈希表,其中双向链表

用来记录缓存数据的访问顺序,哈希表用来实现对数据项的快速访问。

算法实现过程如下:

  1. 如果某个数据项被访问,那么它就被移动到链表的头部(表示最近被使用),如果数据项不在缓存中,则将其添加到链表的头部。
  2. 当缓存达到容量限制时,将链表尾部(表示最近最少被使用)的数据项从缓存中删除。

优点:

  1. 可以尽可能地保留最常用的数据,减少缓存的命中率,提高缓存的效率。
  2. 实现简单,适用于大多数场景,比较容易理解和实现。
  3. 适用于内存有限的情况下,可以避免内存溢出的问题。
  4. 对于热点数据可以快速缓存,避免多次查询,提高系统的性能。

缺点:

  1. 不能保证最佳性能,可能会出现缓存命中率不高的情况。
  2. 当缓存大小达到一定阈值时,需要清除旧数据,如果清除不当可能会导致性能下降。
  3. 实现过程中需要维护一个链表和哈希表,占用一定的内存空间。

LRU缓存算法适用于访问模式比较稳定的场景,例如:热门新闻、热门视频等。同时也适用于内存

有限的场景,可以缓存最常用的数据,避免内存溢出的问题。但是对于访问模式变化频繁的场景,

LRU算法可能无法实现最优的缓存效果,需要根据具体场景选择不同的缓存算法。

二、LFU

1. 原理

Least Recently Used (LRU),中文名为最近最少使用,它的设计原则借鉴了时间局部性原理——

一个对象如果最近没有被使用,那么将来一段时间内都不会被使用,反之亦成立。具体而言,当空

间资源被占用完毕时,如果新增资源加入到空间内部,那么空间内最近未被使用的资源将被调换出

去为新资源腾出一个位置。

Least Frequently Used (LFU),中文名为最不经常使用,它的设计原则是一个对象如果使用频率越

低,那么再次被使用的概率也越低。

因此,当空间资源被占用完毕时,如果新增资源加入到空间内部,那么空间内使用频率最低的资源

将被调换出去为新资源腾出一个位置。

2. 手写必要性

3. 市场调研

4. 代码实现

import java.util.HashMap;
import java.util.LinkedHashSet;

public class LFUCache<K, V> {
    private final int capacity;
    private final HashMap<K, V> keyToVal;
    private final HashMap<K, Integer> keyToFreq;
    private final HashMap<Integer, LinkedHashSet<K>> freqToKeys;
    private int minFreq;

    public LFUCache(int capacity) {
        this.capacity = capacity;
        this.keyToVal = new HashMap<>();
        this.keyToFreq = new HashMap<>();
        this.freqToKeys = new HashMap<>();
        this.minFreq = 0;
    }

    public V get(K key) {
        if (!keyToVal.containsKey(key)) {
            return null;
        }
        increaseFreq(key);
        return keyToVal.get(key);
    }

    public void put(K key, V value) {
        if (capacity <= 0) {
            return;
        }
        if (keyToVal.containsKey(key)) {
            keyToVal.put(key, value);
            increaseFreq(key);
            return;
        }
        if (keyToVal.size() >= capacity) {
            removeMinFreqKey();
        }
        keyToVal.put(key, value);
        keyToFreq.put(key, 1);
        freqToKeys.putIfAbsent(1, new LinkedHashSet<>());
        freqToKeys.get(1).add(key);
        minFreq = 1;
    }

    private void increaseFreq(K key) {
        int freq = keyToFreq.get(key);
        keyToFreq.put(key, freq + 1);
        freqToKeys.get(freq).remove(key);
        freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>());
        freqToKeys.get(freq + 1).add(key);
        if (freqToKeys.get(freq).isEmpty() && freq == minFreq) {
            minFreq = freq + 1;
        }
    }

    private void removeMinFreqKey() {
        LinkedHashSet<K> keyList = freqToKeys.get(minFreq);
        K deletedKey = keyList.iterator().next();
        keyList.remove(deletedKey);
        if (keyList.isEmpty()) {
            freqToKeys.remove(minFreq);
        }
        keyToVal.remove(deletedKey);
        keyToFreq.remove(deletedKey);
    }
}

5. 知识小结

该算法会优先淘汰最近使用次数最少的数据。LFU不同于LRU,它是根据数据的历史访问频率来进

行淘汰数据,而LRU是根据数据最近的访问时间来进行淘汰数据

优点:

  1. 可以有效地利用缓存空间,因为会淘汰使用频率最低的缓存数据,使缓存中保存的数据总是最常用的。
  2. 相对于其他缓存算法,LFU算法更加智能化,因为它可以动态调整使用频率,确保每个缓存数据都是最优的。
  3. LFU算法不会因为某个数据使用频率突然增加而误判,因为它记录的是数据被使用的总次数。

缺点:

  1. LFU算法的实现比较复杂,需要对缓存中的每个数据记录使用的次数。
  2. 需要维护每个数据的使用次数,因此在高并发场景下可能会导致性能问题。
  3. 如果缓存中存在某个数据长时间没有被使用,但是一旦被使用就会频繁地被使用,那么LFU算法可能会将它误判为频繁使用的数据,从而导致缓存淘汰出现问题。

LFU 算法适用于具有以下特点的场景:

  1. 访问频率较高的数据在短时间内仍然有很大概率被再次访问。
  2. 有一部分数据的访问频率特别高,其他数据的访问频率相对较低。
  3. 数据的访问模式具有一定的局部性,即访问一些数据之后,在接下来的一段时间内仍然有较大概率访问与这些数据相关的数据。

三、RR

随机替换 (Random Replacement,RR) 算法的核心思想是随机选择要被替换的缓存块,从而保证

所有缓存块被替换的概率相等。在缓存空间有限的情况下,当需要替换缓存中的某个数据块时,

RR 算法会从当前缓存中随机选择一个数据块进行替换。

优点:

  • 实现简单,容易理解和实现。
  • 在缓存大小较大时表现良好,能够减少缓存替换的次数,提高缓存命中率。

缺点:

  • 算法性能不稳定,在缓存大小较小时,表现较差,因为随机替换可能导致频繁的缓存替换,降低了缓存的命中率。
  • 无法适应不同数据访问模式的需求,不能利用数据局部性进行缓存优化。

适用场景: 随机替换算法适用于数据访问模式比较随机的场景,缓存大小比较大,缓存替换代价

比较高的场景。

例如,在内存比较充足的情况下,使用随机替换算法可以提高缓存命中率,减少缓存替换的次数,

提高系统性能。

但是,在缓存容量较小、数据访问模式具有明显局部性的场景下,随机替换算法的表现会较差。

下面是一个Java 例子:

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;

public class CacheRR<K, V> {

    private int capacity; // 缓存容量
    private HashMap<K, V> map; // 用于存储缓存数据
    private Queue<K> queue; // 用于存储缓存数据的key,以便进行随机替换

    public CacheRR(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>();
        queue = new LinkedList<>();
    }

    /**
     * 从缓存中获取数据
     * @param key 缓存数据的key
     * @return 缓存数据的value,若不存在则返回null
     */
    public synchronized V get(K key) {
        return map.get(key);
    }

    /**
     * 往缓存中添加数据
     * @param key 缓存数据的key
     * @param value 缓存数据的value
     */
    public synchronized void put(K key, V value) {
        // 如果缓存已满,则进行随机替换
        if (map.size() == capacity) {
            K randomKey = queue.poll();
            map.remove(randomKey);
        }
        // 添加新数据
        map.put(key, value);
        queue.offer(key);
    }
    
    /**
     * 获取缓存的大小
     * @return 缓存的大小
     */
    public synchronized int size() {
        return map.size();
    }

}

这段代码实现了一个基于随机替换(RR)算法的缓存,它使用了HashMap来存储缓存数据,并使

用Queue来存储缓存数据的key。

当缓存达到容量上限时,会从队列中随机选择一个key进行替换,以保证替换的公平性。

四、FIFO

先进先出(First-In-First-Out, FIFO)缓存算法是一种比较简单的缓存淘汰算法,它将最早进入缓

存的数据先出去,也就是先进入缓存的数据先被淘汰。

FIFO 算法的实现很简单,只需要使用一个队列来记录进入缓存的顺序,每次新的数据被加入缓存

时,将它放到队列的尾部,淘汰数据时,从队列的头部取出即可。

优点:

  1. 实现简单,易于理解和部署;
  2. 适用于大多数场景,特别是短期的缓存数据;
  3. 缓存命中率高,因为先进入缓存的数据会更早的被使用。

缺点:

  1. 不适用于长期存储数据的场景,因为缓存中的数据可能已经过时;
  2. 当缓存大小不足时,容易产生替换过多的情况,从而降低了缓存的效率;
  3. 缓存的命中率不如其他高级算法,如LRU和LFU。

适用的场景:FIFO缓存算法适用于对缓存数据更新不频繁、缓存大小要求不高的场景

下面是一个使用 Java 实现 FIFO 缓存算法的示例代码:

import java.util.*;

public class FIFOCache<K, V> {
    private final int capacity;
    private final Queue<K> queue;
    private final Map<K, V> cache;

    public FIFOCache(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList<>();
        this.cache = new HashMap<>();
    }

    public void put(K key, V value) {
        // 如果缓存已满,先淘汰最早加入的数据
        if (cache.size() == capacity) {
            K oldestKey = queue.poll();
            cache.remove(oldestKey);
        }

        // 加入新数据
        queue.offer(key);
        cache.put(key, value);
    }

    public V get(K key) {
        return cache.get(key);
    }
}

上面代码使用了一个 Queue 来记录进入缓存的顺序,使用了一个 Map 来记录缓存的数据。当缓存已满时,从

队列头部取出最早加入的数据,并从缓存中移除;当需要获取数据时,直接从缓存中获取即可。

-------------------------------------------

参考文献

  • Java手写LRU缓存算法
  • java语言手动实现LFU算法策略及原理分析
  • 面试官让我手写个LRU缓存,结果...
  • Java中 4种 强大的缓存
  • Java本地高性能缓存的几种实现方式
  • 面试题之java缓存总结,从单机缓存到分布式缓存架构

http://www.kler.cn/a/550560.html

相关文章:

  • 蓝桥杯之最短路径算法
  • 【苍穹外卖】学习
  • 冒险岛079 V8 整合版源码搭建教程+IDEA启动
  • leetcode:643. 子数组最大平均数 I(python3解法)
  • SQL复习
  • 从零开始部署DeepSeek:基于Ollama+Flask的本地化AI对话系统
  • 【深度学习】环境和分布偏移
  • 如何用「教小狗」和「自动驾驶」讲明白 PPO 强化学习?
  • jetson orin nano super AI模型部署之路(一)deepseek r1模型部署
  • 什么是 SQL 注入?
  • Java:单例模式(Singleton Pattern)及实现方式
  • 如何commit后更新.gitignore实现push
  • 1-14 Merge与rebase操作
  • Linux:TCP和守护进程
  • Docker 与持续集成 / 持续部署(CI/CD)的集成(二)
  • Java 大视界 -- 开源社区对 Java 大数据发展的推动与贡献(91)
  • 【HF设计模式】07-适配器模式 外观模式
  • 【Elasticsearch】`nested`字段和`join`字段的区别
  • 三菱PLC实现100楼双电梯控制
  • Linux 外设驱动 应用 6 陀螺仪实验