Guava Cache vs Caffeine 面试详解

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)

    缺点:对突发流量、扫描型访问敏感,热点数据易被冲掉

    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 对新数据不友好的问题。

  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 分布下尤其明显)

原因总结:

摆脱分段锁,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)是当下主流方案

相关推荐
程序员小远2 小时前
Python自动化测试框架及工具详解
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
啦哈拉哈2 小时前
Leetcode题解记录-hot100(81-100)
算法·leetcode·职场和发展
kyriewen3 小时前
大文件上传最全指南:分片、断点续传、秒传,一篇就够了
前端·javascript·面试
小智老师PMP3 小时前
零基础能不能考PMP?零基础专属学习路径+全套扶持体系
学习·算法·职场和发展·软件工程·求职招聘·敏捷流程
我叫黑大帅3 小时前
解决聊天页内部滚轮改为页面滚动问题
javascript·后端·面试
Purple Coder5 小时前
MgB2论文草稿1
职场和发展
Yeyu5 小时前
Android 渲染流水线全解析:从 Choreographer 到 SurfaceFlinger
面试
Java编程爱好者7 小时前
OpenEvent:事件驱动、日志先行的Agent框架
面试
destinying7 小时前
前端秒变AI全栈,我的核心资产是一套Node.js“中间件”
前端·后端·面试