TL;DR

  • 场景: Java 项目中使用 Guava Cache 做本地缓存,线上出现 OOM、命中率异常、线程阻塞和性能回退等疑难问题
  • 结论: 核心问题集中在过期与容量策略配置失误、maximumSize/maximumWeight 误用、CacheLoader 阻塞链路以及 recordStats 滥用
  • 产出: 从配置到监控的一整套排查路径

版本矩阵

组件版本范围已验证
Guava Cache23.0 – 33.0-jre
JDK8, 11, 17
部署形态Spring Boot 2.x / 3.x

疑难问题

1. 是否会OOM?

Guava Cache 可能导致 OOM 的情况:

场景一:缓存永不过期或过期时间过长

Cache<String, Object> cache = CacheBuilder.newBuilder()
    .expireAfterWrite(Long.MAX_VALUE, TimeUnit.DAYS)  // 几乎永不过期
    .build();

场景二:未限制缓存容量或容量设置过大

Cache<String, LargeObject> cache = CacheBuilder.newBuilder()
    .maximumSize(Integer.MAX_VALUE)  // 相当于无限制
    .build();

2. 到期会立刻清除?

Guava Cache 采用的是惰性清理机制

清理触发时机

  • 当执行 get() 操作读取缓存时
  • 当执行 put() 操作写入缓存时
  • 当执行 size() 操作统计缓存大小时

3. maximumSize / maximumWeight 配置不当导致缓存命中率异常低

根本原因分析

  • maximumSize 设置过小
  • weight 配置缺失

正确配置建议

Cache<String, Object> cache = CacheBuilder.newBuilder()
    .maximumWeight(102400)
    .weigher((k,v) -> ((String)v).length())
    .recordStats()
    .build();

4. CacheLoader / get 使用不当,导致阻塞或级联异常

线程阻塞问题

  • 当多个线程并发调用 cache.get(key) 请求同一个未缓存的 key 时
  • 所有线程都会阻塞等待第一个线程完成

5. recordStats 滥用 + 频繁读取统计值

问题根因分析

  1. recordStats 机制的开销: 每次缓存操作都需要对多个计数器进行原子更新
  2. 频繁读取统计值的问题: 业务代码在关键路径中频繁调用 cache.stats() 方法

错误速查表

症状根因定位修复
堆内存持续上涨直至 OOM缓存永不过期或过期时间极长为所有缓存配置合理的 expireAfterWrite 和 maximumSize
已配置过期策略但过期数据长时间仍占内存惰性删除未触发在定时任务中调用 cache.cleanUp()
缓存命中率长期低于 30%maximumSize 设置过小以”热点 key 数量 × 安全系数”重新计算 maximumSize
高并发下大量线程阻塞CacheLoader.load 内部包含慢 SQL将重 I/O 从 load 中拆出并限流
开启 recordStats 后 QPS 下降 5–15%recordStats 为每次操作维护原子计数recordStats 仅在压测或排障环境开启