1339 字
约 4 分钟
0
缓存淘汰算法与 Caffeine 实战:FIFO、LRU、LFU 与 W-TinyLFU

缓存的容量是有限的,当缓存满了,就必须淘汰一些旧数据来腾出空间。**缓存淘汰算法(Cache Eviction Policy)**就是决定"该淘汰谁"的策略。选错淘汰算法,缓存命中率会大幅下降,甚至比没有缓存还糟。

本文先讲三种经典淘汰算法(FIFO、LRU、LFU),再介绍现代 Java 缓存库 Caffeine 所采用的 W-TinyLFU,以及它如何把它们的优点结合起来。

一、为什么要"淘汰"

无论 Redis 还是本地缓存(如 Caffeine),内存都是稀缺资源:

  • 缓存永远装不下所有数据,容量满了就得移除部分条目;
  • 淘汰的目标是保住"最可能再次被访问"的数据,让缓存命中率最高;
  • 不同的淘汰算法,就是在"预测未来访问"这件事上给出不同的判断依据。

二、三种经典淘汰算法

1. FIFO(先进先出)

原理:最先进入缓存的数据最先被移除,就像排队,先进先走。

  • 优点:实现最简单,只需一个队列记录进入顺序;
  • 缺点:完全不考虑数据的使用频率和重要性,经常使用的数据也可能因为"来得早"而被换出,命中率较差。

2. LRU(最近最少使用)

原理:最近最少使用的数据项被优先移除。它假设"最近被访问过的数据,很可能很快还会被访问"。

  • 优点:能较好地反映访问的时间局部性,命中率高,是最常用的淘汰算法;
  • 缺点:需要维护"最近使用"的状态(通常用哈希表 + 双向链表,每次访问要移动节点),需要额外的空间和计算开销。

3. LFU(最不经常使用)

原理:最不经常使用的数据项被优先移除,即访问次数最少的先淘汰。

  • 优点:依据使用频率决策,对"稳定高频"的数据非常友好;
  • 缺点:需要为每个条目维护使用计数,实现复杂度高;且存在"历史问题"——过去很热、现在已冷的数据长期占着计数,新晋热点难以进入缓存。

三者对比一览

算法 淘汰依据 实现复杂度 优点 缺点
FIFO 进入时间 低 简单 不关心访问频率,命中率差
LRU 最近访问时间 中 贴合时间局部性,通用性好 需要维护访问顺序,有额外开销
LFU 访问频率 高 对稳定热点友好 计数开销大,历史频率拖累新数据

三、Caffeine 介绍

Caffeine 是一个基于 Java 8 的高性能本地缓存库,可以看作 Guava Cache 的"高配升级版",也是 Spring Boot 默认集成的本地缓存实现(替代 Guava Cache)。

它最核心的亮点,是采用了 W-TinyLFU 淘汰算法。

1. W-TinyLFU 是什么

W-TinyLFU(Window Tiny LFU)是 LFU 的工程化改进,思路是:

  • 用**频率草图(Frequency Sketch)**近似统计每个 key 的访问频率,内存开销极小;
  • 引入一个窗口段(Window):新数据先进窗口,即使频率低也有机会被访问;窗口溢出的数据再按频率进入主段,从而缓解 LFU"历史频率压制新数据"的问题;
  • 频率会随时间衰减,避免"曾经很热、现在已冷"的数据长期霸占缓存。

简单说:W-TinyLFU ≈ LFU 的频率统计 + LRU 的时间局部性 + 对突发流量的容忍,在命中率上通常优于纯 LRU / 纯 LFU。

2. Caffeine 的核心特性

  • 高性能:接近内存访问极限的读写性能;
  • 自动淘汰:支持基于大小、写入时间、访问时间等多种淘汰策略;
  • 异步加载:支持 LoadingCache、AsyncLoadingCache,缓存未命中时自动加载;
  • 统计:recordStats() 可统计命中率等指标;
  • 事件监听:条目被淘汰/更新时可触发回调。

3. 快速上手

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>

基本用法(按大小淘汰 + 写入后过期):

Cache<String, User> cache = Caffeine.newBuilder()
        .maximumSize(10_000)              // 容量上限,触发淘汰
        .expireAfterWrite(Duration.ofMinutes(5)) // 写入 5 分钟后过期
        .recordStats()                     // 开启命中率统计
        .build();

// 读:先查缓存,未命中再查库并回填
User user = cache.get(userId, id -> userService.getById(id));

配合本地缓存 + Redis 的二级缓存场景(如前面笔记服务所述),Caffeine 正是承担"本地缓存"这一层的最佳选择。

四、总结

  • FIFO 简单但命中率差;LRU 是通用首选;LFU 对稳定热点更准但复杂且怕"历史频率";
  • Caffeine 采用 W-TinyLFU,用频率草图 + 窗口段 + 频率衰减,在极低开销下逼近"最优淘汰",是 Java 本地缓存的现代默认选择;
  • 选淘汰算法本质是用缓存容量换取命中率,理解了三种经典算法,再看 W-TinyLFU 就水到渠成。
缓存淘汰算法与 Caffeine 实战:FIFO、LRU、LFU 与 W-TinyLFU
https://www.clxhxhhr.top/posts/4591/
作者
clxstart
发布于
2026-10-10
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。