TL;DR

  • 场景: 在高并发 Java 服务中使用 Guava Cache 做本地缓存,同时需要控制刷新时延与内存占用
  • 结论: 合理设置 concurrencyLevel + refreshAfterWrite,并理解 LoadingCache 的单 key 加锁语义
  • 产出: 并发参数选型思路、refresh 阻塞行为理解框架

版本矩阵

组件 / 能力版本 / 范围已验证
JDK 81.8.x
JDK 1111.x
JDK 1717.x
Guava Cache23.x–32.x+

并发设置

并发级别参数详解

  1. concurrencyLevel 指定了缓存内部使用的分段锁数量(默认值为4)
  2. 每个分段独立管理一部分缓存条目,不同分段可以并发操作
  3. 合理设置该值可以显著减少线程竞争(建议设置为预估并发线程数的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 定时刷新数据的配置项:

  1. 后台会启动一个异步线程去回源获取最新数据
  2. 在刷新期间,所有对该缓存项的请求会被阻塞(block),默认阻塞时间为 1 分钟
  3. 刷新过程中只有一个请求会实际执行回源操作
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 / 回源逻辑过慢优化回源逻辑、增加超时与降级