缓存淘汰机制LRU和LFU的区别

缓存淘汰机制LRU和LFU的区别

  • 在高并发,高访问量的电商平台中,缓存是提升性能的的保障用户体验的关键;
  • 然而,受限于物理内存,缓存空间总是有限的;
  • 因此必须采用合适的淘汰(替换)策略,保证最有价值的数据能长时间驻留缓存。

LRU与LFU的基本原理

LRU(最少最近使用)

  • 思路:优先淘汰最近一段时间最久没有被访问的数据;
  • 实现:常用哈希表+双向链表;每当访问或者新增数据,即把该数据节点移到链表头部,淘汰是直接移除链表尾部;
  • 优点:实现简单,适应访问热点快速变换的场景。
  • 当我们连续插入DBCA时,此时内存以及满了;
  • 那么当我们再插入一个M的时候,此时内存存放最久的数据D淘汰掉
  • 当我们从外部读取数据M的时候,此时M就会提到头部,这时候M就是最晚被淘汰掉的。

LFU(最不经常使用)

  • 思路:优先淘汰一段时间内访问次数(频率)最低的数据;
  • 实现:需要维护每个数据节点的访问频率,多用哈希表加"频率链表"组合。淘汰最低频率中的最旧节点。
  • 优点:能够更好保持长期高频访问的数据,适合长期热门但访问分布分散的场景。
  • 当我们插入ABC之后,其会以CBA的形式存储于双向链表中;
  • 当我们再插入ABDE后,BA会到频率为2处,ED会到频率为1处;
  • 当再次插入AMD,之后内存会满,如果此时我们再插入一个Q,会先淘汰频率为1处,最先插入的C。

实现方法和复杂性

LRU LFU
原理 淘汰最久未被访问的数据 淘汰访问次数最少的数据
复杂度 O(1) O(1)合理实现时
优点 实现简单,时间开销小 保护长期高频数据,减少冷数据回流
缺点 容易"误杀"最近高配数据 实现较复杂,突发热点响应慢

电商场景下如何选择?

电商常见的数据访问模式:

  • 秒杀,大促,首页推荐:短时间部分商品或页面极度火爆,热点变换块;
  • 长期商品,个性化推荐:部分数据长期有较低频率访问。

选择建议:

  • 若面向热点商品突变频繁场景(如秒杀,活动页)--优先选择LRU。因为持续被访问的热点商品会留在缓存前端,发生访问突变事能够快速适用变化,防止缓存穿透;
  • 若需要保护"常青"商品或内容库(如个性化,长期售卖页面)--可选择LFU。能够留存虽然访问不集中的长期高配数据,防止配LRU "误杀";
  • 实际项目中采用分区或者多级缓存,针对不如业务分别设计缓存策略(如活动页LRU,推荐页LFU)。

结论

  • LRU和LFU的根本区别:一个重"新近性",一个重"访问频率";
  • 电商访问模式以热点突变为主,推荐优先使用LRU缓存机制;
  • 综合业务需求时,可以混合使用LRU/LFU,或者采用2Q,LRU-K等改进型算法,结合流量特征和数据重要性灵活选型。

代码实现

LRU缓存(基于双向链表+哈希表)

java 复制代码
/**
 *  LRU缓存(基于双向链表+哈希表)
 */
public class LRU<K,V> {
    private final int capacity;
    private final Map<K, Node<K,V>> map;
    private final Node<K,V> head,tail;
    
    static class Node<K,V>{
        K key;
        V value;
        Node<K,V> prev,next;
        
        Node(){}
        Node(K key,V value){
            this.key = key;
            this.value = value;
        }
    }
    
    public LRU(int capacity){
        this.capacity = capacity;
        this.map = new HashMap<>();
        head = new Node<>();
        tail = new Node<>();
        head.next = tail;
        tail.prev = head;
    }
    
    public V get(K key){
        Node<K,V> node = map.get(key);
        if(node == null){
            return null;
        }
        moveToHead(node);
        return node.value;
    }
    
    
    public void put(K key,V value){ 
        Node<K,V> node = map.get(key);
        if(node == null){ 
            node = new Node<>(key,value);
            map.put(key,node);
            addToHead(node);
            if(map.size() > capacity){
                Node<K,V> tail = removeTail();
                map.remove(tail.key);
            }else {
                node.value = value;
                addToHead(node);
            }
        }
    }

    /**
     * 添加节点
     * @param node
     */
    private void addToHead(Node<K,V> node){
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    
    /**
     * 删除节点
     * @param node
     */
    private void removeNode(Node<K,V> node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    /**
     * 移动节点到头部
     * @param node
     */
    private void moveToHead(Node<K,V> node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
        addToHead(node);
    }
    
    /**
     * 删除尾部节点
     * @return
     */
    private Node<K,V> removeTail(){
        Node<K,V> node = tail.prev;
        node.prev.next = tail;
        tail.prev = node.prev;
        return node;
    }
    
}

LFU缓存(基于HashMap+频率链表)

java 复制代码
/**
 * LFU缓存(基于HashMap+频率链表)
 */
public class LFU<K,V> {
    private int capacity;
    private int minFreq;
    private final Map<K, Node<K, V>> nodeMap;
    private final Map<Integer, LinkedHashSet<Node<K, V>>> freqMap;

    static class Node<K, V> {
        K key;
        V value;
        int freq;

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

    public LFU(int capacity) {
        this.capacity = capacity;
        this.minFreq = 0;
        this.nodeMap = new HashMap<>();
        this.freqMap = new HashMap<>();
    }

    public V get(K key) {
        Node<K, V> node = nodeMap.get(key);
        if (node == null) return null;
        increaseFreq(node);
        return node.value;
    }

    public void put(K key, V value) {
        if (capacity <= 0) return;
        if (nodeMap.containsKey(key)) {
            Node<K, V> node = nodeMap.get(key);
            node.value = value;
            increaseFreq(node);
            return;
        }

        if (nodeMap.size() == capacity) {
            // 淘汰访问频率最低且最久的节点
            LinkedHashSet<Node<K, V>> set = freqMap.get(minFreq);
            Node<K, V> toRemove = set.iterator().next();
            set.remove(toRemove);
            nodeMap.remove(toRemove.key);
        }

        Node<K, V> newNode = new Node<>(key, value);
        nodeMap.put(key, newNode);
        freqMap.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(newNode);
        minFreq = 1;
    }

    private void increaseFreq(Node<K, V> node) {
        int freq = node.freq;
        LinkedHashSet<Node<K, V>> set = freqMap.get(freq);
        set.remove(node);
        if (freq == minFreq && set.isEmpty()) {
            minFreq++;
        }
        node.freq++;
        freqMap.computeIfAbsent(node.freq, k -> new LinkedHashSet<>()).add(node);
    }
}

测试案例

java 复制代码
public class CacheTest {
    public static void main(String[] args) {
        // 测试LRU缓存
        LRU<Integer, String> lruCache = new LRU<>(2);
        lruCache.put(1, "A");
        lruCache.put(2, "B");
        System.out.println(lruCache.get(1)); // 输出: A
        lruCache.put(3, "C");
        System.out.println(lruCache.get(2)); // 输出: null (2被淘汰)

        // 测试LFU缓存
        LFU<Integer, String> lfuCache = new LFU<>(2);
        lfuCache.put(1, "A");
        lfuCache.put(2, "B");
        System.out.println(lfuCache.get(1)); // 输出: A
        lfuCache.put(3, "C");
        System.out.println(lfuCache.get(2)); // 输出: null (2被淘汰,因1 频率高)
    }
}
相关推荐
guslegend18 天前
说说你对设计模式的理解
大厂面试专题
guslegend18 天前
设计模式是如何分类的
大厂面试专题
guslegend23 天前
说说你对泛型的理解
大厂面试专题
guslegend1 个月前
Java中变量和常量有什么区别
大厂面试专题
guslegend1 个月前
String类能被继承吗,为什么
大厂面试专题
guslegend1 个月前
HashMap和Hashtable有什么区别
大厂面试专题
guslegend1 个月前
Java中止线程的三种方式
大厂面试专题
guslegend1 个月前
Java五种文件拷贝方式
大厂面试专题
guslegend1 个月前
提示词工程能够解决什么问题?
大厂面试专题