Guava Cache vs Caffeine 面试详解


一、背景与起源

  • Guava Cache:Google 开源工具库 Guava 的子模块,2011 年发布,长期是 Java 本地缓存事实标准。
  • Caffeine:由 Guava Cache 的核心贡献者 Ben Manes 主导,基于 JDK 8 完全重写。Spring Boot 2.x 起取代 Guava 成为默认本地缓存实现。

面试金句:Caffeine 可以理解为 "Guava Cache 2.0",是同一作者的进化版


二、核心架构差异

1. 数据结构

| 维度 | Guava Cache | Caffeine | |---|---|---| | 底层 | 类似 ConcurrentHashMap(JDK7 分段锁思想) | 基于 JDK8 ConcurrentHashMap + 环形缓冲区(RingBuffer) | | 并发控制 | 分段锁 Segment(默认 4 段) | CAS + 无锁化设计 | | 读写记录 | 直接在读写路径同步处理 | 读写事件先写入 RingBuffer,异步批量处理 |

2. 淘汰算法(面试高频
  • Guava:分段 LRU(Least Recently Used)
    • 缺点:对突发流量、扫描型访问敏感,热点数据易被冲掉
  • CaffeineW-TinyLFU(Window Tiny LFU)
    • 由一个小的 LRU 窗口(Window,约 1%)+ 主区域 SLRU(Probation/Protected)组成
    • 用 Count-Min Sketch(类布隆过滤器结构)记录访问频率,仅 4 bit/key,内存开销极小
    • 命中率接近最优算法(Belady),实测比 LRU 高 10%~30%

面试金句:Caffeine 用空间极小的频率草图弥补了 LRU 不考虑频率的缺陷,又通过窗口机制解决了 LFU 对新数据不友好的问题

3. 过期清理
  • Guava:纯惰性清理,读写时顺带清理少量条目。问题:长时间无访问 → 过期数据驻留内存。
  • Caffeine:基于 时间轮(Hierarchical Timer Wheel) 实现,O(1) 复杂度定位到期 key,配合 ForkJoinPool 异步清理,更及时。

三、功能特性对比

| 特性 | Guava Cache | Caffeine | |---|---|---| | 最大容量限制 | ✅ maximumSize / maximumWeight | ✅ 同左 | | 基于时间过期 | expireAfterWrite / expireAfterAccess | ✅ 额外支持 expireAfter(可变过期) | | 引用类型 | weakKeys / weakValues / softValues | ✅ 同左 | | 同步加载 | LoadingCache | ✅ 同左 | | 异步加载 | ❌ 不支持 | ✅ AsyncLoadingCache(CompletableFuture) | | 刷新机制 | refreshAfterWrite(阻塞当前线程) | refreshAfterWrite(异步刷新不阻塞) | | 统计 | recordStats() | ✅ 更丰富 | | 监听器 | RemovalListener(同步) | ✅ 异步执行,不影响主流程 | | 可变过期时间 | ❌ | ✅ Expiry 接口,每个 key 可不同 |


四、性能对比(作者 JMH 基准)

  • 读吞吐:Caffeine ≈ Guava 的 6 倍
  • 写吞吐:Caffeine ≈ Guava 的 4~5 倍
  • 混合读写:差距更明显
  • 命中率:W-TinyLFU > LRU(Zipf 分布下尤其明显)

原因总结:

  1. 摆脱分段锁,JDK8 CHM 性能更优
  2. 读写事件异步批处理,主路径无锁
  3. 算法本身命中率更高 → 减少回源开销

五、API 示例对比

Guava:

LoadingCache<String, User> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .recordStats()
    .build(new CacheLoader<>() {
        public User load(String key) { return loadFromDB(key); }
    });

Caffeine:

LoadingCache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(Duration.ofMinutes(10))
    .refreshAfterWrite(Duration.ofMinutes(5))  // 异步刷新
    .recordStats()
    .build(key -> loadFromDB(key));

API 几乎一致,迁移成本极��,这是面试常问点。


六、优缺点总结

Guava Cache

  • 优点:生态成熟、与 Guava 工具库一体、老项目零成本
  • 缺点:性能低、LRU 命中率一般、不支持异步、已停止重大更新

Caffeine

  • 优点:性能最强、W-TinyLFU 高命中率、异步友好、Spring 官方推荐、作者持续维护
  • 缺点:要求 JDK 8+、需额外依赖、相对年轻(但已非常成熟)

七、面试可能追问

  1. 为什么 Caffeine 性能比 Guava 高? → 异步事件处理(RingBuffer) + 无分段锁 + 时间轮过期 + 更优算法。

  2. W-TinyLFU 算法原理? → Count-Min Sketch 记频率 + Window LRU 接纳新数据 + SLRU 主存储;新老 PK 用频率判定是否准入。

  3. refreshAfterWrite 和 expireAfterWrite 区别? → expire 到期 key 不可用必须重新加载(阻塞);refresh 到期后返回旧值并异步刷新(Caffeine),用户无感知。

  4. 本地缓存的局限? → 节点间数据不一致、容量受 JVM 限制、GC 压力 → 分布式场景需 Redis 或多级缓存(Caffeine + Redis)。

  5. 如何防止缓存击穿/穿透? → LoadingCache 内置 单飞(singleflight),同一 key 并发只回源一次;穿透用空值缓存或布隆过滤器。

  6. Caffeine 在 Spring Boot 中如何使用?spring.cache.type=caffeine + spring.cache.caffeine.spec=... 配合 @Cacheable


八、选型建议(面试结论)

  • 新项目:无脑选 Caffeine
  • 已用 Guava 且无性能瓶颈:可保留
  • 需多级缓存:Caffeine(L1) + Redis(L2)是当下主流方案
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐