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

被折叠的 条评论
为什么被折叠?



