TL;DR
- 场景: Java 项目中使用 Guava Cache 做本地缓存,线上出现 OOM、命中率异常、线程阻塞和性能回退等疑难问题
- 结论: 核心问题集中在过期与容量策略配置失误、maximumSize/maximumWeight 误用、CacheLoader 阻塞链路以及 recordStats 滥用
- 产出: 从配置到监控的一整套排查路径
版本矩阵
| 组件 | 版本范围 | 已验证 |
|---|---|---|
| Guava Cache | 23.0 – 33.0-jre | 是 |
| JDK | 8, 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 滥用 + 频繁读取统计值
问题根因分析:
- recordStats 机制的开销: 每次缓存操作都需要对多个计数器进行原子更新
- 频繁读取统计值的问题: 业务代码在关键路径中频繁调用
cache.stats()方法
错误速查表
| 症状 | 根因定位 | 修复 |
|---|---|---|
| 堆内存持续上涨直至 OOM | 缓存永不过期或过期时间极长 | 为所有缓存配置合理的 expireAfterWrite 和 maximumSize |
| 已配置过期策略但过期数据长时间仍占内存 | 惰性删除未触发 | 在定时任务中调用 cache.cleanUp() |
| 缓存命中率长期低于 30% | maximumSize 设置过小 | 以”热点 key 数量 × 安全系数”重新计算 maximumSize |
| 高并发下大量线程阻塞 | CacheLoader.load 内部包含慢 SQL | 将重 I/O 从 load 中拆出并限流 |
| 开启 recordStats 后 QPS 下降 5–15% | recordStats 为每次操作维护原子计数 | recordStats 仅在压测或排障环境开启 |