TL;DR

  • 场景: 线上项目广泛使用 Guava Cache,但对 LocalCache / Segment / LoadingCache 具体行为缺乏源码级认知
  • 结论: Guava 通过 LocalCache+Segment 分段结构、引用队列和访问/写入队列,实现并发安全的本地缓存、过期与刷新策略
  • 产出: 一份围绕体系类图、put/get 流程与过期重载机制的工程化笔记

体系类图

Guava Cache 的核心类结构:

  • CacheBuilder: 缓存构建器,指定配置参数并初始化本地缓存
  • CacheLoader: 抽象类,用于从数据源加载数据
  • Cache: 接口,定义 get、put、invalidate 等操作
  • LoadingCache: 接口,继承自 Cache,定义 get、getUnchecked、getAll 等操作
  • LocalCache: 核心类,包含数据结构与基本缓存操作方法
  • LocalManualCache: LocalCache 内部静态类,实现 Cache 接口
  • LocalLoadingCache: LocalCache 内部静态类,继承 LocalManualCache,实现 LoadingCache 接口

LocalCache 核心结构

LocalCache 为 Guava Cache 的核心类,数据结构与 ConcurrentHashMap 相似,由多个 Segment 组成:

主要字段:

  • Segment<K, V>[] segments: Map 的数组
  • int concurrencyLevel: 并发量,即 segments 数组大小
  • long expireAfterAccessNanos: 访问后的过期时间
  • long expireAfterWriteNanos: 写入后的过期时间
  • long refreshNanos: 刷新时间

Segment 组成:

  • AtomicReferenceArray<ReferenceEntry<K, V>> table: 哈希表
  • Queue<ReferenceEntry<K, V>> writeQueue: 写入顺序队列
  • Queue<ReferenceEntry<K, V>> accessQueue: 访问顺序队列

Put 流程分析

  1. 上锁: 加锁保证当前 Segment 内 put 操作的线程安全
  2. 清理队列元素: 清理 keyReferenceQueue 和 valueReferenceQueue 两个引用队列
  3. setValue: 将 value 写入到 Entry
V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock();
    try {
        long now = map.ticker.read();
        preWriteCleanup(now);
    } finally {
        unlock();
        postWriteCleanup();
    }
}

Get 流程分析

  1. 获取对象引用(可能是非 alive 状态)
  2. 判断对象引用是否是 alive 的
  3. 如果是 alive 的且设置 refresh,则异步刷新查询 value
  4. 针对不是 alive 但正在 loading 的,等待 loading 完成
  5. 如果 value 还未拿到,则查询 loader 方法获取对应值
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = hash(checkNotNull(key));
    return segmentFor(hash).get(key, hash, loader);
}

过期重载机制

数据过期不会自动重载,而是通过 get 操作时执行过期重载。

LocalLoadingCache 核心方法:

  • get(K key): 不存在则通过 CacheLoader 加载
  • getUnchecked(K key): 不声明受检异常
  • getAll(Iterable<? extends K> keys): 批量加载
  • refresh(K key): 主动刷新

错误速查

症状根因定位修复
缓存偶发读到旧数据仅配置 expireAfterWrite/Access,依赖懒失效配置 refreshAfterWrite 或显式 refresh
size() 在高并发下明显跳动Segment.count + modCount 设计为弱一致快照避免用 size() 做容量/限流判断
QPS 突增时偶发长尾请求CacheLoader 执行时间长优化数据源查询或引入本地合并加载
JVM 堆占用持续升高未设置 maximumSize/maximumWeight明确设置容量与权重策略