缓存04:Caffeine

从本地缓存优势、W-TinyLFU 淘汰算法到与 Redis 多级缓存架构,拆解 Caffeine 在高性能本地缓存场景下的核心机制。

字数 1471 阅读时长 ≈ 5 分钟 2026-7-13 2026-7-13
缓存04:Caffeine

Caffeine 不是”分布式缓存”,而是”高性能本地缓存”。它的核心场景是热点数据本地缓存、高频读场景、降低远程缓存压力,支持内存级读写、W-TinyLFU 淘汰算法和异步刷新,适合单机热点数据、配置缓存、计算结果缓存。

这篇文章围绕三个核心点:本地缓存优势、W-TinyLFU 淘汰算法、与 Redis 多级缓存架构,把 Caffeine 的设计逻辑和适用场景讲清楚。

为什么需要本地缓存

分布式缓存(Redis、Memcached)虽然解决了多实例共享数据的问题,但存在以下瓶颈:

  • 网络开销:每次读取都需要网络请求,延迟增加
  • 热点压力:热点 Key 集中访问,Redis 单节点压力大
  • 单点故障:Redis 故障时缓存不可用,数据库压力激增

本地缓存的优势:

维度本地缓存(Caffeine)分布式缓存(Redis)
访问延迟微秒级(内存访问)毫秒级(网络请求)
吞吐量百万级 QPS十万级 QPS
热点压力无(本地内存)热点 Key 压力大
数据一致性弱(多实例不一致)强(集中存储)
数据共享不支持(单机)支持(分布式)
内存容量受限于 JVM 堆内存受限于服务器内存

简单判断:热点数据、高频读、一致性要求不高 → 本地缓存合适。数据共享、一致性要求高、数据量大 → 分布式缓存合适。

Caffeine 核心机制:W-TinyLFU 淘汰算法

本地缓存的核心问题:内存有限,如何选择淘汰策略,保证热点数据不丢失

传统淘汰策略的局限

淘汰策略说明局限
LRU(Least Recently Used)淘汰最近最少使用的数据无法处理扫描访问(大量一次性数据挤掉热点)
LFU(Least Frequently Used)淘汰访问频率最低的数据无法处理热点变化(历史热点一直占用内存)
FIFO先进先出不考虑访问频率和时效

传统 LRU/LFU 在面对复杂访问模式时,命中率下降。

W-TinyLFU 算法

Caffeine 使用 W-TinyLFU(Window TinyLFU) 算法,结合 LRU 和 LFU 的优势:

  • Window(窗口区):新数据进入窗口区,采用 LRU 策略
  • Main(主区):热点数据进入主区,采用 LFU 策略
  • Probation(试用期):窗口区淘汰的数据进入试用期,与主区淘汰的数据竞争,胜者留下

W-TinyLFU 的优势:

优势说明
抗扫描访问一次性数据只在窗口区,不会挤掉主区热点
适应热点变化主区采用衰减的频率计数,历史热点会被新热点替换
高命中率结合 LRU 和 LFU,命中率高于传统算法

Caffeine 的命中率接近理论最优,在高频读场景下性能优异。

Caffeine 配置示例

Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10000)                    // 最大缓存数量
    .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后 10 分钟过期
    .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后 5 分钟过期
    .refreshAfterWrite(1, TimeUnit.MINUTES) // 写入后 1 分钟异步刷新
    .recordStats()                          // 开启统计
    .build(key -> loadUserFromDb(key));     // 缓存未命中时加载数据

配置说明:

配置项说明
maximumSize最大缓存数量,超过时淘汰
expireAfterWrite写入后过期时间,适合数据不变的场景
expireAfterAccess访问后过期时间,适合热点数据
refreshAfterWrite写入后异步刷新,避免读时阻塞
recordStats开启命中率统计,监控缓存效果

多级缓存架构:Caffeine + Redis

本地缓存和分布式缓存不是互斥的,而是互补的。生产环境常采用多级缓存架构:

请求 → Caffeine(L1 本地缓存)→ Redis(L2 分布式缓存)→ 数据库

多级缓存流程

  1. 请求先查 Caffeine:命中则返回,未命中则查 Redis
  2. Redis 未命中则查数据库:数据库返回后写入 Redis 和 Caffeine
  3. 数据更新时:先更新数据库,再删除 Redis,最后删除 Caffeine

一致性问题

多级缓存的一致性比单级缓存更复杂:

一致性问题说明解决方式
Caffeine 与 Redis 不一致Caffeine 未及时更新,读到旧数据设置较短过期时间、监听 Redis 删除事件
多实例 Caffeine 不一致不同实例的 Caffeine 数据不一致使用消息广播(Redis Pub/Sub)通知其他实例删除

多级缓存配置示例

使用 Spring Cache + Caffeine + Redis:

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // L1 本地缓存(Caffeine)
        CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES));
        
        // L2 分布式缓存(Redis)
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)))
            .build();
        
        // 组合缓存管理器
        CompositeCacheManager compositeCacheManager = new CompositeCacheManager(
            caffeineCacheManager, redisCacheManager);
        return compositeCacheManager;
    }
}

Caffeine 使用场景

场景说明
配置缓存:系统配置、业务配置本地缓存,减少 Redis 压力
计算结果缓存:复杂计算结果缓存,避免重复计算
热点数据缓存:高频读数据本地缓存,降低延迟
限流计数:本地限流计数器,减少远程存储压力

不适合场景:

  • 数据共享:多实例需要共享数据
  • 数据一致性要求高:多实例数据必须一致
  • 数据量大:超过 JVM 堆内存容量

常见误区

误区真实情况
本地缓存不需要考虑一致性多实例本地缓存不一致,需要广播删除或短过期时间
Caffeine 可以替代 Redis本地缓存不能共享,适合作为 Redis 的补充
W-TinyLFU 是完美的算法复杂,内存开销比 LRU 大,但命中率更高
本地缓存容量可以很大受限于 JVM 堆内存,过大影响 GC 性能

项目判断

引入 Caffeine 时,先回答三个问题:

  1. 访问频率:是否是热点数据,高频读?
  2. 一致性要求:是否允许短暂数据不一致?
  3. 内存容量:数据量是否在 JVM 堆内存容量内?

如果是热点数据、高频读、允许短暂不一致、数据量小 → Caffeine 合适。如果数据需要共享、一致性要求高、数据量大 → Redis 合适。两者可以组合使用:Caffeine 做 L1 本地缓存,Redis 做 L2 分布式缓存。

线上排查 Caffeine 问题:

  • 命中率低:检查缓存策略,是否过期时间过短、淘汰过快
  • 内存占用大:检查缓存数量,控制 maximumSize,避免影响 GC
  • 缓存未命中阻塞:使用 refreshAfterWrite 异步刷新,避免读时阻塞