Guava Cache的refreshAfterWrite机制
Guava Cache 的 refreshAfterWrite
机制在源码中通过 惰性刷新调度 和 细粒度锁控制 实现,核心逻辑集中在 LocalCache
类的 Segment
结构中。以下是关键源码解析:
一、核心数据结构
1. Segment
类
• 继承自 ReentrantLock
:每个 Segment
独立加锁,控制并发访问。
• 内部数据结构:
• AtomicReferenceArray<ReferenceEntry<K, V>> table
:存储缓存条目的哈希表。
1. AtomicReferenceArray
• 定义:Java 并发包 java.util.concurrent.atomic
中的类,提供 原子操作的引用数组,支持无锁化的 CAS(Compare-And-Swap)操作。
• 特性:
• 线程安全:通过 CAS 保证对数组元素的原子性更新,避免传统锁的开销。
• 固定长度:创建时指定容量,不可动态扩容(Guava Cache 通过分段机制解决容量扩展问题)。
2. ReferenceEntry<K, V>
• 角色:缓存条目的数据结构,存储键值对(K
和 V
)及元数据(如访问时间、过期时间、哈希值等)。
• 链表结构:通过 next
指针形成链表,解决哈希冲突(类似 ConcurrentHashMap
的链地址法)。
• 5 个队列(访问队列、写入队列等):管理缓存项的回收策略。
二、触发刷新的关键方法
1. get
方法入口
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
// 1. 根据 hash 获取 Segment
Segment<K, V> segment = segmentFor(hash);
// 2. 调用 Segment 的 get 方法
return segment.get(key, hash, loader);
}
• 关键逻辑:通过 hash
定位到具体的 Segment
,由 Segment
处理并发和刷新。
2. Segment#get
方法
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
// 1. 尝试获取缓存条目
ReferenceEntry<K, V> e = getEntry(key, hash);
V value = getLiveValue(e); // 检查是否过期
if (value != null) {
// 2. 检查是否需要刷新
if (scheduleRefresh(e, key, hash, value, now, loader)) {
return value; // 返回旧值,异步刷新
}
return value;
}
// 3. 缓存未命中,加锁加载新值
return lockedGetOrLoad(key, hash, loader);
}
• 核心逻辑:
• 步骤 1:通过 getEntry
获取缓存条目。
• 步骤 2:若条目存在且未过期,调用 scheduleRefresh
判断是否触发异步刷新。
• 步骤 3:若条目不存在或过期,加锁加载新值(阻塞式加载)。
3. scheduleRefresh
方法
boolean scheduleRefresh(ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) {
// 1. 检查是否配置了 refreshAfterWrite
if (!map.refreshAfterWrite()) return false;
// 2. 计算是否超过刷新时间
long refreshTime = entry.getWriteTime() + map.refreshNanos;
if (now >= refreshTime) {
// 3. 触发异步刷新
refresh(key, hash, loader, false);
return true;
}
return false;
}
• 关键逻辑:
• 判断是否超过 refreshAfterWrite
时间。
• 若超时,调用 refresh
方法异步加载新值。
4. refresh
方法
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
// 1. 加锁,确保只有一个线程执行加载
Lock lock = segment.lock();
try {
// 2. 检查是否已被其他线程加载
ReferenceEntry<K, V> entry = getEntry(key, hash);
if (entry == null) return null;
// 3. 调用 CacheLoader.reload 异步加载
ListenableFuture<V> future = loader.reload(key, getLiveValue(entry));
if (future != null) {
// 4. 提交到线程池执行
ListenableFutureTask<V> task = ListenableFutureTask.create(future);
executor.execute(task);
return task.get(); // 阻塞等待结果(仅当前线程)
}
} finally {
lock.unlock();
}
return null;
}
• 关键逻辑:
• 加锁:通过 Segment
锁保证同一 Key 的刷新串行化。
• 异步加载:调用 CacheLoader.reload
返回 ListenableFuture
,提交到独立线程池执行。
• 返回旧值:当前线程直接返回旧值,其他线程不受影响。
三、异步刷新的线程模型
1. 线程池配置
• 默认行为:使用 MoreExecutors.directExecutor()
(同步执行)。
• 推荐配置:通过 CacheBuilder.refreshAfterWrite
指定独立线程池:
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<Key, Value>() {
@Override
public ListenableFuture<Value> reload(Key key, Value oldValue) {
return executor.submit(() -> loadNewValue(key)); // 异步加载
}
});
2. ListenableFuture
机制
• 非阻塞等待:主线程不阻塞,其他请求直接返回旧值。
• 异常处理:若异步加载失败,后续请求会触发重试。
四、锁与并发控制
1. 细粒度锁
• 锁粒度:仅对当前 Segment
加锁,而非全局锁。
• 锁竞争:不同 Key 的刷新操作在不同 Segment
中并行执行。
2. CAS 操作
• 原子性更新:通过 AtomicReferenceArray
保证缓存条目的原子性更新。
五、与 expireAfterWrite
的差异
逻辑 | refreshAfterWrite 源码实现 | expireAfterWrite 源码实现 |
---|---|---|
触发条件 | 检查 refreshAfterWrite 时间,超时则触发异步刷新 | 检查 expireAfterWrite 时间,超时则直接阻塞加载 |
锁控制 | 加锁后异步加载,其他线程返回旧值 | 加锁后同步加载,其他线程阻塞等待 |
线程模型 | 独立线程池执行刷新任务 | 无独立线程池,依赖请求线程加载 |
六、总结
Guava Cache 的 refreshAfterWrite
机制通过以下设计实现高性能:
- 惰性刷新:仅在访问时触发刷新判断,避免后台轮询。
- 异步加载:通过
ListenableFuture
和独立线程池解耦 I/O 操作。 - 细粒度锁:
Segment
级锁减少锁竞争,提升并发能力。 - 旧值返回:刷新期间其他线程仍可读取旧值,避免阻塞。
适用场景:读多写少、允许短暂不一致(如配置信息、商品详情页)。