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)是当下主流方案

相关推荐
swipe7 小时前
16|(前端转全栈)前端人排查后端问题:curl、traceId、日志、MySQL、Redis 怎么用?
前端·后端·面试
用户0510122572967 小时前
DAY 5-智能指针与 C++ 内存管理
c++·面试
FogLetter7 小时前
进程VS线程:你的电脑到底是怎么同时干那么多活的?
前端·面试
FogLetter8 小时前
我真的写了个“诈尸式”缓存组件:手撕React KeepAlive
前端·react.js·面试
Lzh编程小栈9 小时前
【STM32底层精讲】RCC时钟系统超全详解(时钟树+源码+避坑指南)
c语言·stm32·单片机·嵌入式硬件·面试
艾莉丝努力练剑10 小时前
【MYSQL】MYSQL学习的一大重点:事务(上)- 原子性与持久性
android·数据库·学习·mysql·面试
西安小哥1 天前
AI时代全栈面试通关指南:从背八股到聊架构
面试·ai编程
咩咩啃树皮1 天前
第52篇:前端工程化完整体系精讲——Vite构建、模块化、打包原理、规范体系、面试终极通关
前端·面试·职场和发展
FogLetter1 天前
当JavaScript学会“分身术”:异步编程的进化简史
前端·javascript·面试
InKomorebi1 天前
Agent 五大工程体系:Prompt、Context、Loop、Graph 与 Harness
面试·llm