Local Cache在业务场景中是不可或缺的一部分。当需要频繁的查询某些非实时变化的数据时,就可以考虑使用缓存, 主要有以下几种场景:
查询具有K-V特性;获取的数据非实时变化;频繁的查询请求;内存使用量可接受;我们通过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方法,将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值,不存在阻塞;