Guava Cache
场景
Local Cache在业务场景中是不可或缺的一部分。当需要频繁的查询某些非实时变化的数据时,就可以考虑使用缓存, 主要有以下几种场景:
- 查询具有K-V特性;
- 获取的数据非实时变化;
- 频繁的查询请求;
- 内存使用量可接受;
LoadingCache
public LoadingCache<String, String> localCache() {
return CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.refreshAfterWrite(20, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println("缓存不存在,加载ing->" + key);
String result = key + ":one";
return result;
}
});
}
我们通过CacheBuilder 生成了一个LoadingCache缓存对象
- maximumSize定义了缓存的容量大小,当缓存数量即将到达容量上限时,则会进行缓存回收,回收最近没有使用或总体上很少使用的缓存项。需要注意的是在接近这个容量上限时就会发生,所以需要选取比合适maximumSize稍大的值。
- expireAfterWrite这个方法定义了缓存的过期时间:expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收;expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。
- load方法:当获取的缓存值不存在或已过期时,则会调用此load方法,进行缓存值的计算。
- refreshAfterWrite(long, TimeUnit):定时刷新(Guava cache的刷新需要依靠用户请求线程,让该线程去进行load方法的调用,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新),这个方法非常重要,看到过很多没有使用定时刷新而带来服务大量阻塞的情况。我们知道LoadingCache对“缓存穿透”做了控制,即当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成;然而,这样阻塞在高并发时,会造成大量的阻塞,一招不慎,甚至线程池耗尽;所以请记得一定要使用refreshAfterWrite方法:请求某个key缓存时,更新线程调用load方法更新该缓存,其他请求线程返回该缓存的旧值。这样只有一个线程被阻塞,用来生成缓存值,而其他的线程不会被阻塞。
异步刷新
上述方式其实已经能够绝大满足生产条件需要了,对于数据库/某些密集计算的负担已经大大减小,但是对于某些特定的业务场景,某个线程的一次阻塞意味着一次请求失败,可能也是不可容忍的,这个时候异步刷新是一种更好的选择:
@Configuration
class LocalCacheConfig {
ListeningExecutorService reloadPool =
MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10 , 20, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(100)));
@Bean
public LoadingCache<String, String> localCache() {
return CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS)
.refreshAfterWrite(20, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
System.out.println("缓存不存在,加载ing->" + key);
String result = key + ":one";
return result;
}
@Override
public ListenableFuture<String> reload(String key,
String oldValue) throws Exception {
return reloadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("缓存不存在,加载ing->reload");
return load(key);
}
});
}
});
}
该实现重写了CacheLoader的reload方法,将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,不存在阻塞;