TL;DR
- 场景: 在高并发 Java 服务中使用 Guava Cache 做本地缓存,同时需要控制刷新时延与内存占用
- 结论: 合理设置 concurrencyLevel + refreshAfterWrite,并理解 LoadingCache 的单 key 加锁语义
- 产出: 并发参数选型思路、refresh 阻塞行为理解框架
版本矩阵
| 组件 / 能力 | 版本 / 范围 | 已验证 |
|---|---|---|
| JDK 8 | 1.8.x | 是 |
| JDK 11 | 11.x | 是 |
| JDK 17 | 17.x | 是 |
| Guava Cache | 23.x–32.x+ | 是 |
并发设置
并发级别参数详解
- concurrencyLevel 指定了缓存内部使用的分段锁数量(默认值为4)
- 每个分段独立管理一部分缓存条目,不同分段可以并发操作
- 合理设置该值可以显著减少线程竞争(建议设置为预估并发线程数的1.5倍)
底层实现原理
采用分段锁(Striped Lock)技术实现,将整个缓存划分为多个Segment:
Cache<String, Object> cache = CacheBuilder.newBuilder()
.concurrencyLevel(8) // 设置为8个分段
.maximumSize(1000)
.build();
性能优化建议
- 低并发场景(<4线程):使用默认值即可
- 中等并发(4-16线程):建议设置为8-16
- 高并发场景(>16线程):需要根据实际压力测试调整
更新锁定
Guava Cache 提供了 refreshAfterWrite 定时刷新数据的配置项:
- 后台会启动一个异步线程去回源获取最新数据
- 在刷新期间,所有对该缓存项的请求会被阻塞(block),默认阻塞时间为 1 分钟
- 刷新过程中只有一个请求会实际执行回源操作
CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return fetchDataFromDB(key);
}
});
注意:
- 与 expireAfterWrite 不同,refreshAfterWrite 不会自动移除过期数据
自定义LRU
class LRUCache<K,V> extends LinkedHashMap<K, V> {
private final int limit;
public LRUCache(int limit) {
super(limit, 0.75f, true);
this.limit = limit;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > limit;
}
}
错误速查
| 症状 | 根因 | 修复 |
|---|---|---|
| 并发线程数上来后,缓存命中变差 | concurrencyLevel 设置过大 | 将 concurrencyLevel 控制在预估并发线程数附近 |
| 以为配置了 refreshAfterWrite 后,过期数据会自动清除 | 混淆 refreshAfterWrite 与 expireAfterWrite | 同时配置 expireAfterWrite 或主动 invalidate |
| 高峰期 read 被卡住 | load / 回源逻辑过慢 | 优化回源逻辑、增加超时与降级 |