Guava Cache在日常开发中的使用及问题

本文深入解析GuavaCache的原理与使用,涵盖缓存构建、回收策略、更新机制及线程安全处理。通过实例演示如何利用GuavaCache提高数据查询效率,避免线程阻塞,实现高效缓存管理。

1.Guava Cache概念

Guava cahe是一个线程安全的本地缓存。当通过缓存查询数据时,未找到所要查询的数据,通过执行自定义数据读取方法获取数据,并将结果存入缓存。通过锁避免多线程重复加载。最终通过应用本地缓存提高查询速度。

Guava cache的主要功能如下:

  • 对数据未命中,可加载进缓存
  • 当缓存数据过期,可移除
  • 可根据缓存读取或写入的时间更新过期时间
  • 可统计缓存的命中率、异常等数据

2.使用Guava Cache

通过系统的CacheBuilder来构建缓存对象,并在build方法中指定一个CacheLoader,并实现load方法。形式如下:

LoadingCache<String,String> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<String,String>() {
    @Override
    public String load(String key) throws Exception {
        return key.toUpperCase();
    }
});
System.out.println(loadingCache.getUnchecked("hello"));
System.out.println(loadingCache.getUnchecked("hello"));

当通过key从缓存获取数据时,如果数据不存在,则通过CacheLoader加载数据,返回key的大写的形式,并将数据写入缓存。当下次再次获取,则直接从缓存返回,无需计算。

当通过build方法并传入CacheLoader构建缓存时,返回的是LoadingCache。

除了最简单的形式,还可以指定缓存的过期时间与过期发生时的过期事件监听器。

  • expireAfterWrite:指定过期时间
  • removalListener:监听key清除事件
 @Test
 public void createCache() throws Exception{
     LoadingCache<String,String> loadingCache = CacheBuilder.newBuilder()
             .expireAfterWrite(2,TimeUnit.SECONDS)
             .removalListener(n -> System.out.println("过期key:" + n.getKey()))
             .build(new CacheLoader<String,String>() {
         @Override
         public String load(String key) throws Exception {
             return key.toUpperCase();
         }
     });

     System.out.println(loadingCache.getUnchecked("hello"));
     TimeUnit.SECONDS.sleep(2);
     System.out.println(loadingCache.getUnchecked("hello"));
     TimeUnit.SECONDS.sleep(2);
     System.out.println(loadingCache.getUnchecked("hello"));
     TimeUnit.MINUTES.sleep(5);
 }

缓存创建完毕,2秒后将失效。

HELLO
过期key:hello
HELLO
过期key:hello
HELLO

除了常规使用外,get方法还提供额外的参数,指定callable参数。和前面一种方法比较,这种方式可以在get数据时,根据key灵活指定如何加载数据。

V get(K key, Callable<? extends V> loader) throws ExecutionException;

通过这个参数,当get数据未获取到时,可以用来加载数据。

    @Test
    public void callableCacheTest() throws Exception{
        Cache<String,String> loadingCache = CacheBuilder.newBuilder()
                .expireAfterWrite(1,TimeUnit.SECONDS)
                .removalListener(n -> System.out.println("过期key:" + n.getKey()))
                .build();

        System.out.println(loadingCache.get("hello",() -> "a"));
        TimeUnit.SECONDS.sleep(2);
        System.out.println(loadingCache.get("hello",() -> "a"));
        TimeUnit.SECONDS.sleep(2);
        System.out.println(loadingCache.get("world",() -> "b"));
        TimeUnit.SECONDS.sleep(2);
        System.out.println(loadingCache.get("world",() -> "b"));
        TimeUnit.MINUTES.sleep(5);
    }
}
a
过期key:hello
a
b
过期key:world
b

3.缓存回收方法

第2节的例子可以看到,我们通过CacheBuilder构建缓存时,通过expireAfterWrite来指定缓存回收策略。此外还可以通过其它不同的方法指定不同的策略。

3.1 定期回收

  • expireAfterAccess(long, TimeUnit):如果缓存数据在指定的时间访问内没有读或写,那么被回收。这种缓存的回收顺序和基于大小回收一样。
  • expireAfterWrite(long, TimeUnit):如果缓存数据在给定时间内没有被写访问(创建或覆盖),那么回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。
    基于引用回收

3.2 基于引用回收

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。

3.3 基于容量回收

  • CacheBuilder.maximumSize(long):指定了最大缓存容量,当缓存存满后,将尝试回收最近没有使用或总体上很少使用的缓存项。此外可以通过maximumWeight与weigher对缓存设定不同的权重,来决定回收顺序。如下通过weigher来计算每个对象的权重,最终决定如何回收。
class Student {
}

LoadingCache<String,Student> loadingCache = CacheBuilder.newBuilder()
		.maximumSize(1000)
        .maximumWeight(500)
        .weigher((String key, Student value) ->  {
            return value.getWeight();
        }).build(new CacheLoader<String, Student>() {
            @Override
            public Student load(String key) throws Exception {
                return getFromDB(key);
            }
        });

3.4 主动回收

我们可以主动调用缓存的清除方法,实现缓存的清除。

Cache<String,String> loadingCache = CacheBuilder.newBuilder()
     .expireAfterWrite(1,TimeUnit.SECONDS)
     .removalListener(n -> System.out.println("过期key:" + n.getKey()))
     .build();
  • 清除某个key:loadingCache.invalidate(key)
  • 批量清除:loadingCache.invalidateAll(keys)
  • 清除所有缓存项:loadingCache.invalidateAll()

4.回收触发时机

通常回收缓存可以通过定时任务或在使用时进行回收。

guava cache在每次进行缓存操作的时候,如get()或者put()的时候,先判断缓存是否过期,如果过期就将其回收。

如果该缓存迟迟没有访问也会存在数据不能被回收的情况。因此要考虑这种情况。

5.缓存更新及可能导致的线程阻塞问题

大量线程用相同的key获取缓存值时,如果此时此刻缓存过期了,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这种场景会导致任务阻塞,因此可能会产生严重的问题。

因此为了避免所有线程阻塞,还可以考虑使用单一线程更新,其余线程返回兜底数值。更新线程来调load方法更新该缓存。其他请求线程返回该缓存的兜底旧值。这样就避免了所有线程阻塞导致可能的严重问题。

LoadingCache<String,Student> loadingCache = CacheBuilder.newBuilder()
  .refreshAfterWrite(5,TimeUnit.SECONDS)
  .build(new CacheLoader<String, Student>() {
      @Override
      public Student load(String key) throws Exception {
          return null;
      }
  });

当用户线程获取数据时,触发load方法调用。其余线程返回就值。如果一直没有请求尝试获取该缓存值,则该缓存也并不会刷新。

由于在刷新的过程中,为防止多线程并发下重复加载,需要先锁定,获得加载资格,再完成加载。单个key大量线程访问,可以通过让一个用户线程去访问这个key,其余key返回旧值的形式来解决大量阻塞问题。

当大量用户线程访问不同key时,由于这种加锁机制加上加载时可能速度会很慢。依然会导致大量不同的线程被阻塞。这种可以考虑使用背景任务线程。让背景任务线程去加载数据。而所有的用户线程先返回旧值。这样解决访问不同key时导致的阻塞问题。

ListeningExecutorService backgroundRefreshPools =
    MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));

LoadingCache<String,Student> loadingCache = CacheBuilder.newBuilder()
   .refreshAfterWrite(5,TimeUnit.SECONDS)
   .build(new CacheLoader<String, Student>() {
       @Override
       public Student load(String key) throws Exception {
           return null;
       }

       @Override
       public ListenableFuture<Student> reload(String key, Student oldValue) throws Exception {
           return backgroundRefreshPools.submit(...)
       }
   });

代码中覆盖reload方法,并通过背景线程处理数据加载。而当用户线程访问时,依然访问的是旧值。这样避免大量阻塞。

6. 总结

使用缓存,由于容量有限,需要考虑在满额的情况下使用何种缓存淘汰策略。当数据不未命中时,要考虑如何刷新数据到缓存。而刷新数据到缓存时,要考虑避免大量线程调用刷新方法使服务不可用,以及在使用在锁保护的机制下可能产生的大量线程阻塞的问题。

7. 参考

1.https://github.com/google/guava
2.https://www.baeldung.com/guava-cache

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值