Caffeine 更适合高并发读场景,因其采用 Window TinyLfu 淘汰策略,相比 Guava 的 LRU 更抗热点污染、延迟更低、内存更稳;写入性能高20%~35%(关闭recordStats时),且支持更精细的过期与刷新控制。

为什么 Caffeine 比 Guava Cache 更适合高并发读场景
Caffeine 在高吞吐读操作下延迟更低、内存占用更稳,核心在于它用 Window TinyLfu 替代了 Guava 的 LRU + 基于引用的淘汰策略。LRU 容易被偶发热点数据污染缓存,而 TinyLfu 能识别并过滤掉“一次写入、极少再读”的噪声条目。
-
maximumSize和maximumWeight不能共存,设错会抛IllegalStateException - 默认不开启弱引用(
weakKeys()/weakValues()),若业务对象生命周期短且 key 是临时对象,不加可能引发 OOM - 写入性能比 Guava 高约 20%~35%,但前提是关闭
recordStats()—— 开启后每次 get/put 都要原子计数,压测时 QPS 可能跌 15%
如何正确配置 Caffeine 的过期与刷新策略
过期(expireAfterWrite / expireAfterAccess)和刷新(refreshAfterWrite)不是互斥的,但行为差异大:前者是“过期即删”,后者是“过期后异步重建,旧值仍可返回”。线上常见误配是把 refreshAfterWrite 当成自动续期,结果缓存击穿没防住。
-
expireAfterWrite(10, TimeUnit.MINUTES)是强约束,超时立刻不可见;refreshAfterWrite(10, TimeUnit.MINUTES)不影响可见性,只触发后台 reload - 必须配合
removalListener或cache.asMap().cleanUp()主动清理失效 entry,否则size()可能虚高 - 使用
refreshAfterWrite时,CacheLoader的reload方法必须是非阻塞的,否则线程池耗尽风险极高
Spring Boot 中集成 Caffeine 的三个关键配置点
Spring Cache 抽象层对 Caffeine 的支持看似简单,但几个配置项不显式指定,就容易退化成无过期、无统计、无监听的“裸 Map”。
- 必须在
@Configuration类里声明CaffeineCacheManagerBean,并调用setCaffeine(Caffeine.newBuilder()....),光靠spring.cache.caffeine.spec配置项无法启用recordStats()或removalListener -
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=60s里的单位必须小写(s,不是S),否则解析失败且静默忽略 - 若用
@Cacheable(sync = true),底层依赖的是CacheLoader的load方法,此时refreshAfterWrite不生效 —— sync 模式下只走 load,不走 reload
Caffeine 缓存穿透与雪崩的实际防护姿势
Caffeine 是本地缓存,天生不解决穿透和雪崩,但可以配合业务逻辑做轻量级兜底。很多人试图用 expireAfterWrite 加随机抖动来防雪崩,结果发现抖动值太小没效果,太大又导致缓存命中率骤降。
立即学习“Java免费学习笔记(深入)”;
- 穿透防护:在
CacheLoader.load()里对空结果也缓存(如Optional.empty()),并设较短过期时间(expireAfterWrite(2, TimeUnit.SECONDS)),避免反复查 DB - 雪崩防护:用
Caffeine.newBuilder().refreshAfterWrite(30, TimeUnit.SECONDS)+CacheLoader.asyncReload(),让刷新异步化,同时限制刷新线程数(通过自定义Executor) - 注意
Cache.stats()的missCount和loadExceptionCount必须定期上报,否则无法判断是穿透还是下游服务异常
缓存配置里最易被跳过的其实是移除监听器的错误处理逻辑 —— RemovalListener 抛异常不会中断主线程,但会导致后续该 key 的 remove 事件永远丢失,监控毛刺就从这儿开始。











