GuavaCache
(一) 应用场景
本地缓存:高并发场景下,使用guavaCache实现本地缓存,提高系统吞吐量,降低数据库压力。
(二)简介
guavaCache是Goole guava库中一个组件,支持多种配置:设置缓存大小、过期策略、且是线程安全的,无需额外同步控制;
(三) 基本特性
- 线程安全:从数据库加载数据时,只允许一个线程去加载,防止缓存过期时大量请求打到数据库,造成雪崩;
- 自动加载:缓存失效时,会自动调用指定方法加载;
- 过期策略:基于时间的过期策略;包括创建后(expire after write)的过期和访问后的过期(expire after access)
- 惰性删除&惰性加载:缓存过期时不会立即被删除;设置的expire after write到期后reload方法也不会立刻去执行。只有在有请求进来时且缓存未过期,reload方法才会执行
- 本质是一个ConcurrentMap
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
}
(四) 示例(1)
代码:
public void test3() throws InterruptedException{final AtomicInteger COUNTER = new AtomicInteger(0);ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(8) // 根据实际情况调整线程池大小);LoadingCache<String, String> loadingCacheMap = CacheBuilder.newBuilder().initialCapacity(10) // 初始容量
// .maximumSize(8) // 设定最大容量.expireAfterWrite(10L, TimeUnit.SECONDS) // 设定写入过期时间
// .concurrencyLevel(8) // 设置最大并发写操作线程数.refreshAfterWrite(5L, TimeUnit.SECONDS) // 设定自动刷新数据时间.build(new CacheLoader<String, String>() {@Overridepublic String load(@NotNull String key) {log.info("load方法执行");try {return fetchDataFromDB(key);} catch (Exception e) {log.error("从数据库加载出现异常,key为:{},异常为:", key, e);}return null;}@Overridepublic ListenableFuture<String> reload(String key, String oldValue) {log.info("reload方法执行");return refreshPool.submit(() -> {return fetchDataFromDB(key); // 重新从数据库加载数据});}public String fetchDataFromDB(String key) {try {Thread.sleep(2000);Random random = new Random();String value = key+ random.nextInt();log.info("新生成的value值为:{}",value);return value;} catch (Exception e) {log.error("fetchDataFromDB出现异常");}return null;}});ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);for (int i = 0; i <= 4; i++) {pool.scheduleAtFixedRate(()-> {try {long start = System.currentTimeMillis();int threadNo = COUNTER.getAndIncrement() % 10; // 自己算编号Thread.currentThread().setName("worker-" + threadNo);log.info("异步线程开始");log.info("获取到的结果:{},耗时:{}", loadingCacheMap.get("aaaa"), System.currentTimeMillis() - start);} catch (ExecutionException e) {throw new RuntimeException(e);}},0,13,TimeUnit.SECONDS);}new CountDownLatch(1).await(2,TimeUnit.MINUTES);log.info("阻塞结束,主线程继续");}
执行结果:
代码说明
- 过期策略是创建10s后过期,5s后有请求访问的话,执行reload方法;
- 定时任务是13s跑一次,所以,每次定时任务访问缓存都是过期的;
- 模拟并发场景,多个线程同时,访问缓存同一条数据,日志【load方法执行】和日志【新生成的value值为xxx】每次只打印一次,说明guavaCache是有并发控制的,同一时刻只能有一个线程执行更新操作(执行load方法);且每次都是执行的load方法(未执行reload方法,什么时候执行?看下面的示例),说明过期时间是生效的,符合预期;
(五)示例(2)
代码
public void test2() throws InterruptedException {final AtomicInteger COUNTER = new AtomicInteger(0);ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(8) // 根据实际情况调整线程池大小);LoadingCache<String, String> loadingCacheMap = CacheBuilder.newBuilder().initialCapacity(10) // 初始容量
// .maximumSize(8) // 设定最大容量.expireAfterWrite(10L, TimeUnit.SECONDS) // 设定写入过期时间
// .concurrencyLevel(8) // 设置最大并发写操作线程数.refreshAfterWrite(5L, TimeUnit.SECONDS) // 设定自动刷新数据时间.build(new CacheLoader<String, String>() {@Overridepublic String load(@NotNull String key) {log.info("load方法执行");try {return fetchDataFromDB(key);} catch (Exception e) {log.error("从数据库加载出现异常,key为:{},异常为:", key, e);}return null;}@Overridepublic ListenableFuture<String> reload(String key, String oldValue) {log.info("reload方法执行");return refreshPool.submit(() -> {return fetchDataFromDB(key); // 重新从数据库加载数据});}public String fetchDataFromDB(String key) {try {Thread.sleep(2000);Random random = new Random();String value = key+ random.nextInt();log.info("新生成的value值为:{}",value);return value;} catch (Exception e) {log.error("fetchDataFromDB出现异常");}return null;}});ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);pool.scheduleAtFixedRate(()-> {try {long start = System.currentTimeMillis();int threadNo = COUNTER.getAndIncrement() % 10; // 自己算编号Thread.currentThread().setName("worker-" + threadNo);log.info("异步线程开始");log.info("获取到的结果:{},耗时:{}", loadingCacheMap.get("aaaa"), System.currentTimeMillis() - start);} catch (ExecutionException e) {throw new RuntimeException(e);}},0,13,TimeUnit.SECONDS);new CountDownLatch(1).await(2,TimeUnit.MINUTES);log.info("阻塞结束,主线程继续");}
执行结果
代码说明
- 过期策略是创建10s后过期,5s后有请求访问的话,执行reload方法;
- 定时任务13秒跑一次,每次访问缓存已过期,应重新加载
- 模拟串行场景,每次异步线程开始,都会打印重新加载的相关日志,说明过期时间是生效的;
- 定时任务13s跑一次,缓存是10s过期,但是在定时任务访问时没有从缓存中直接取,而是重新加载,说明guavaCache是懒加载的,只有在有请求时,才会加载和更新缓存;
- 那配置的5秒,以及reload方法什么时候生效和执行呢?请看示例(3)
(六) 示例(3)
代码(以串行执行为例)
public void test2() throws InterruptedException {final AtomicInteger COUNTER = new AtomicInteger(0);ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(8) // 根据实际情况调整线程池大小);LoadingCache<String, String> loadingCacheMap = CacheBuilder.newBuilder().initialCapacity(10) // 初始容量
// .maximumSize(8) // 设定最大容量.expireAfterWrite(10L, TimeUnit.SECONDS) // 设定写入过期时间
// .concurrencyLevel(8) // 设置最大并发写操作线程数.refreshAfterWrite(5L, TimeUnit.SECONDS) // 设定自动刷新数据时间.build(new CacheLoader<String, String>() {@Overridepublic String load(@NotNull String key) {log.info("load方法执行");try {return fetchDataFromDB(key);} catch (Exception e) {log.error("从数据库加载出现异常,key为:{},异常为:", key, e);}return null;}@Overridepublic ListenableFuture<String> reload(String key, String oldValue) {log.info("reload方法执行");return refreshPool.submit(() -> {return fetchDataFromDB(key); // 重新从数据库加载数据});}public String fetchDataFromDB(String key) {try {Thread.sleep(2000);Random random = new Random();String value = key+ random.nextInt();log.info("新生成的value值为:{}",value);return value;} catch (Exception e) {log.error("fetchDataFromDB出现异常");}return null;}});ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);pool.scheduleAtFixedRate(()-> {try {long start = System.currentTimeMillis();int threadNo = COUNTER.getAndIncrement() % 10; // 自己算编号Thread.currentThread().setName("worker-" + threadNo);log.info("异步线程开始");log.info("获取到的结果:{},耗时:{}", loadingCacheMap.get("aaaa"), System.currentTimeMillis() - start);} catch (ExecutionException e) {throw new RuntimeException(e);}},0,8,TimeUnit.SECONDS);new CountDownLatch(1).await(2,TimeUnit.MINUTES);log.info("阻塞结束,主线程继续");}
执行结果
代码说明
-
过期策略是创建10s后过期,5s后有请求访问的话,执行reload方法;
-
定时任务是8秒跑一次,理论上,每次访问不会过期
-
模拟串行场景:可以看到,定时任务第一访问时(第0秒),缓存为空,执行load方法加载数据;
-
第二次访问时(第8秒),缓存还未过期,从缓存中取旧值,且可以看到耗时很短,毫秒级别(可以说明是从缓存中取的值且取到的值和上一次的值一样);
-
同时,在第8秒,此线程打印了【reload方法执行】(说明reload方法执行;我们配置的是5s执行,但是在第8s执行,说明reload方法也是懒加载的,只在有请求时执行)
-
与示例(1)对比,为什么示例(3)执行了reload方法,示例(1)没有执行,是因为示例(3)中,定时任务在访问缓存中,缓存还未过期,所以才会执行reload方法
-
同时,在第8秒第二次定时任务访问缓存时,获取完数据后,下面有一个异步线程(pool-19-thread-1),打印了【新生成的value值】,这说明执行了reload方法;而且,第三次定时任务取到的就是第二次异步线程加载的新值
-
从图中也可以看到,除第一次外,每次定时任务取到的值都是上一次定时任务的异步线程生成的新值
-
线程名称是pool-19-thread-xx 这种的都是异步线程
-
因此,若每次访问缓存数据还没过期且设置了refreshAfterWrite参数,那么之后,只会执行reload方法,不会再执行load方法了
(七) 总结
- load方法和reload方法都是懒加载的,只有在请求到达时,才可能执行;
- load方法和reload方法都是线程安全的,同一时刻只能有一个线程执行;
- reload方法的执行时机是要保证访问的缓存还在有效期内,才能执行reload方法;若访问的缓存已经过了有效期,那只会执行load方法
- 在高并发场景下,使用refreshAfterWrite(reload方法的执行频率)参数是为了给缓存续命,减少数据库的压力,所以该参数的设置应该小于expireAfterWrite(load方法的执行频率,创建后的过期时间)参数,这样才能达到目的。
(八) 彩蛋
- 有个问题,如果设置的过期是10s(expireAfterWrite参数),自动刷新时间(refreshAfterWrite参数)是5s,定时任务每2s访问缓存一次,那么guavaCache是怎么判断在第几秒执行reload方法呢?
- 个人拙见:设自动刷新时间是5s,过期时间是17:25:30,当前访问时间是17:25:24,当前请求进来时,判断当前时间与过期时间的差值是否小于配置的自动刷新时间,即17:25:30-17:25:24=6s,不小于,则不执行reload;若小于,则执行reload方法。