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",是同一作者的进化版。
二、核心架构差异
-
数据结构
| 维度 | Guava Cache | Caffeine |
|---|---|---|
| 底层 | 类似 ConcurrentHashMap(JDK7 分段锁思想) | 基于 JDK8 ConcurrentHashMap + 环形缓冲区(RingBuffer) |
| 并发控制 | 分段锁 Segment(默认 4 段) | CAS + 无锁化设计 |
| 读写记录 | 直接在读写路径同步处理 | 读写事件先写入 RingBuffer,异步批量处理 |
-
淘汰算法(面试高频)
Guava:分段 LRU(Least Recently Used)
缺点:对突发流量、扫描型访问敏感,热点数据易被冲掉
Caffeine:W-TinyLFU(Window Tiny LFU)
由一个小的 LRU 窗口(Window,约 1%)+ 主区域 SLRU(Probation/Protected)组成
用 Count-Min Sketch(类布隆过滤器结构)记录访问频率,仅 4 bit/key,内存开销极小
命中率接近最优算法(Belady),实测比 LRU 高 10%~30%
面试金句:Caffeine 用空间极小的频率草图弥补了 LRU 不考虑频率的缺陷,又通过窗口机制解决了 LFU 对新数据不友好的问题。
-
过期清理
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 分布下尤其明显)
原因总结:
摆脱分段锁,JDK8 CHM 性能更优
读写事件异步批处理,主路径无锁
算法本身命中率更高 → 减少回源开销
五、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+、需额外依赖、相对年轻(但已非常成熟)
七、面试可能追问
为什么 Caffeine 性能比 Guava 高?
→ 异步事件处理(RingBuffer) + 无分段锁 + 时间轮过期 + 更优算法。
W-TinyLFU 算法原理?
→ Count-Min Sketch 记频率 + Window LRU 接纳新数据 + SLRU 主存储;新老 PK 用频率判定是否准入。
refreshAfterWrite 和 expireAfterWrite 区别?
→ expire 到期 key 不可用必须重新加载(阻塞);refresh 到期后返回旧值并异步刷新(Caffeine),用户无感知。
本地缓存的局限?
→ 节点间数据不一致、容量受 JVM 限制、GC 压力 → 分布式场景需 Redis 或多级缓存(Caffeine + Redis)。
如何防止缓存击穿/穿透?
→ LoadingCache 内置 单飞(singleflight),同一 key 并发只回源一次;穿透用空值缓存或布隆过滤器。
Caffeine 在 Spring Boot 中如何使用?
→ spring.cache.type=caffeine + spring.cache.caffeine.spec=... 配合 @Cacheable。
八、选型建议(面试结论)
新项目:无脑选 Caffeine
已用 Guava 且无性能瓶颈:可保留
需多级缓存:Caffeine(L1) + Redis(L2)是当下主流方案