淘汰策略之tinyLFU

TinyLFU 缓存原理

1. TinyLFU 原理简述

TinyLFU(Tiny Least Frequently Used)是一种高效的缓存淘汰策略,它结合了 LFU(最不常用)和 LRU(最近最少使用)的优点,主要用于统计访问频率并结合窗口区(LRU)进行缓存管理

核心思想:

  • 使用近似计数器(如 Count-Min Sketch)高效统计 key 的访问频率(但不保存所有 key 的精确频率)。
  • 缓存分为窗口区(LRU)和主区(LFU),新数据先进入窗口区,频率高的数据晋升到主区。
  • 淘汰时,比较新数据和主区中频率最低的数据,决定是否替换。

TinyLFU 适合大规模缓存场景,命中率高,且内存开销低。

2. TinyLFU Demo(简化版)

由于 TinyLFU 需要高效计数(Count-Min Sketch),这里用简单的 HashMap 近似模拟计数器。

实际生产实现(如 Caffeine)会用更复杂的数据结构。

java 复制代码
import java.util.*;

class TinyLFUCache<K, V> {
    private final int windowSize;
    private final int mainSize;
    private final LinkedHashMap<K, V> windowCache; // LRU窗口
    private final LinkedHashMap<K, V> mainCache;   // LFU主区
    private final Map<K, Integer> freqMap;         // 访问频率计数器

    public TinyLFUCache(int capacity) {
        this.windowSize = Math.max(1, capacity / 10); // 10%窗口
        this.mainSize = capacity - windowSize;
        this.windowCache = new LinkedHashMap<>(windowSize, 0.75f, true);
        this.mainCache = new LinkedHashMap<>(mainSize, 0.75f, true);
        this.freqMap = new HashMap<>();
    }

    public V get(K key) {
        if (windowCache.containsKey(key)) {
            freqMap.put(key, freqMap.getOrDefault(key, 0) + 1);
            return windowCache.get(key);
        }
        if (mainCache.containsKey(key)) {
            freqMap.put(key, freqMap.getOrDefault(key, 0) + 1);
            return mainCache.get(key);
        }
        return null;
    }

    public void put(K key, V value) {
        // 增加频率
        freqMap.put(key, freqMap.getOrDefault(key, 0) + 1);

        if (windowCache.containsKey(key) || mainCache.containsKey(key)) {
            // 更新已存在的
            if (windowCache.containsKey(key)) windowCache.put(key, value);
            else mainCache.put(key, value);
            return;
        }

        // 新数据先放窗口区
        if (windowCache.size() < windowSize) {
            windowCache.put(key, value);
        } else {
            // 淘汰窗口区最旧的
            Iterator<K> it = windowCache.keySet().iterator();
            K evictKey = it.next();
            V evictValue = windowCache.get(evictKey);
            it.remove();

            // 晋升淘汰的数据到主区
            int evictFreq = freqMap.getOrDefault(evictKey, 0);
            if (mainCache.size() < mainSize) {
                mainCache.put(evictKey, evictValue);
            } else {
                // 主区满了,找最低频率项
                K minFreqKey = null;
                int minFreq = Integer.MAX_VALUE;
                for (K k : mainCache.keySet()) {
                    int freq = freqMap.getOrDefault(k, 0);
                    if (freq < minFreq) {
                        minFreq = freq;
                        minFreqKey = k;
                    }
                }
                // 淘汰主区最低频率项
                if (evictFreq > minFreq) {
                    mainCache.remove(minFreqKey);
                    mainCache.put(evictKey, evictValue);
                }
                // 如果淘汰的数据频率不高于主区最低频率,则直接丢弃
            }
            // 最后把新 key 放进窗口区
            windowCache.put(key, value);
        }
    }
}

3. 使用示例

java 复制代码
public class Main {
    public static void main(String[] args) {
        TinyLFUCache<Integer, String> cache = new TinyLFUCache<>(5);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        cache.get(1); // 增加1的频率
        cache.put(4, "D");
        cache.put(5, "E");
        cache.get(2); // 增加2的频率
        cache.put(6, "F"); // 触发淘汰
        System.out.println(cache.get(1)); // 可能输出A
        System.out.println(cache.get(2)); // 可能输出B
        System.out.println(cache.get(3)); // 可能null
        System.out.println(cache.get(4)); // 可能输出D
        System.out.println(cache.get(5)); // 可能输出E
        System.out.println(cache.get(6)); // 可能输出F
    }
}

4. 原理总结

  1. 新数据进入窗口区(LRU),窗口满时晋升到主区(LFU)。
  2. 主区淘汰时,比较新数据与主区最低频率的数据,决定替换与否。
  3. 频率计数用近似计数器,实际生产建议用 Count-Min Sketch。
  4. TinyLFU 兼顾访问频率和最近访问时间,命中率高,抗热点污染能力强。

TinyLFU 为什么能兼顾短期热点和长期高频数据

1. 短期热点(窗口区 LRU)

  • **窗口区(Window Cache)**采用 LRU(最近最少使用)策略。
  • 新数据总是先进入窗口区。
  • 如果某个 key 在短时间内被频繁访问(比如突然成为热点),它会一直被窗口区保留,不会被快速淘汰。
  • 这样可以应对"突发热点":比如某个新闻突然很火,相关 key 频繁被访问,能优先缓存这些数据。

2. 长期高频数据(主区 LFU)

  • **主区(Main Cache)**采用 LFU(最不常用)策略。
  • 当窗口区满了,最旧的数据会被淘汰,但淘汰时会晋升到主区(如果它的访问频率高于主区最低频率的数据)。
  • 主区只保留访问频率高的数据。那些长期被频繁访问的数据,其频率计数很高,即使它们暂时"冷却"也不会被淘汰。
  • 这样可以应对"长期热点":比如用户每天都访问的首页、常用功能等,能长期缓存这些数据。

3. 两者结合的优势

  • 短期热点:窗口区及时响应最近访问的数据,避免热点数据被频率统计"稀释"而被淘汰。
  • 长期高频:主区通过访问频率统计,避免短期访问高的数据冲击导致长期高频数据被淘汰。
  • 提升命中率:这种分区和分策略,能让缓存既有"灵活性"(应对热点),又有"稳定性"(保留高频)。

4. 示意图

5. 实际例子说明

  • 某个 key(比如热搜词)突然被大量访问,能迅速进入窗口区并被缓存,用户体验好。
  • 某个 key(比如用户主页)虽然最近访问不多,但长期累计访问很多,频率高,会被主区保留,不容易被淘汰。

6. 结论

TinyLFU 通过窗口区(LRU)和主区(LFU)分区管理 ,实现了对短期热点和长期高频数据的兼顾,有效提升了缓存命中率和抗冲击能力。这也是它比单纯 LRU 或 LFU 更受欢迎的原因。


相关推荐
mit6.8248 小时前
[Tongyi] 工具集成 | run_react_infer
人工智能·深度学习·算法
闻缺陷则喜何志丹8 小时前
【C++贪心】P8769 [蓝桥杯 2021 国 C] 巧克力|普及+
c++·算法·蓝桥杯·洛谷
杨小码不BUG9 小时前
灯海寻踪:开灯问题的C++精妙解法(洛谷P1161)
c++·算法·数学建模·位运算·浮点数·信奥赛·csp-j/s
杨小码不BUG9 小时前
心痛之窗:滑动窗口算法解爱与愁的心痛(洛谷P1614)
开发语言·c++·算法·滑动窗口·csp-j/s·多维向量
咖啡啡不加糖9 小时前
贪心算法详解与应用
java·后端·算法·贪心算法
一只鱼^_10 小时前
力扣第470场周赛
数据结构·c++·算法·leetcode·深度优先·动态规划·启发式算法
CUMT_DJ14 小时前
matlab计算算法的运行时间
开发语言·算法·matlab
KyollBM17 小时前
每日羊题 (质数筛 + 数学 | 构造 + 位运算)
开发语言·c++·算法
Univin19 小时前
C++(10.5)
开发语言·c++·算法