Guava缓存,谷歌开源的一种本地缓存,使用本节点的内存来存储的,实现原理类似于ConcurrentHashMap,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
CacheLoader可以理解为一个固定的加载器,在创建Cache时指定,重写V load(K key) 方法后,当检索不存在的时会自动的加载数据。
package com.example.cache;
import com.google.common.cache.*;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 描述:
* 本地缓存Ddmo
*
* @author XueGuCheng
* @create 2022-11-01 23:03
*/
public class GuavaCacheDemo {
// 模拟DB
private static final HashMap<Integer, String> map = new HashMap<>();
public static LoadingCache<Integer, String> createGuavaCache(){
return CacheBuilder.newBuilder()
// 设置并发级别为5,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(5)
// 设置写缓存后10秒钟后过期
.expireAfterWrite(10, TimeUnit.SECONDS)
// 设置缓存容器的初始容量为8
.initialCapacity(8)
// 设置缓存最大容量为10,超过10之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(10)
// 设置统计缓存的各种统计信息(生产坏境关闭)
.recordStats()
// 设置缓存的移除通知
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
}
})
// 指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(new CacheLoader<Integer, String>() {
@Override
public String load(Integer key) throws Exception {
// 往DB中查询数据
System.out.println("查询key:" + key + "的数据");
return map.get(key);
}
});
}
public static void main(String[] args) throws ExecutionException {
map.put(1,"java");
map.put(2,"天下");
map.put(3,"第一");
LoadingCache<Integer, String> loadingCache = createGuavaCache();
// 第一次缓存中没有数据,所以会往DB中查询数据
System.out.println(loadingCache.get(2));
// 第二次缓存中有数据,CacheLoader.load方法不会加载
System.out.println(loadingCache.get(2));
}
}
运行结果:
Callable在get时可以指定,效果跟CacheLoader一样,区别就是两者定义的时间点不一样,Callable更加灵活。
public static void main(String[] args) throws ExecutionException {
map.put(1,"java");
map.put(2,"天下");
map.put(3,"第一");
LoadingCache<Integer, String> loadingCache = createGuavaCache();
// 第一次缓存中没有数据,所以会往DB中查询数据
// System.out.println(loadingCache.get(2));
// // 第二次缓存中有数据,CacheLoader.load方法不会加载
// System.out.println(loadingCache.get(2));
int i = 3;
String s = loadingCache.get(i, new Callable<String>() {
@Override
public String call() throws Exception {
// 往DB中查询数据
System.out.println("(callable) 查询key:" + i + "的数据");
return map.get(i);
}
});
System.out.println(s);
}
运行结果:
清除策略:超过最大容量之后就会按照LRU最近虽少使用算法来移除缓存项。
public static void main(String[] args) throws ExecutionException {
map.put(1,"java");
map.put(2,"天下");
map.put(3,"第一");
map.put(4,"j");
map.put(5,"a");
map.put(6,"v");
LoadingCache<Integer, String> loadingCache = createGuavaCache();
// 此时缓存的最大容量为4,存放6个数据
for(int i = 1; i <= 6; i++){
System.out.println(loadingCache.get(i));
}
}
运行结果:
使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumWeight(1000)
.weigher(new Weigher<Key, Graph>() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) {
return createExpensiveGraph(key);;
}
});
expireAfterWrite与refreshAfterWrite混合使用情况:
例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
map.put(1,"java");
map.put(2,"天下");
map.put(3,"第一");
map.put(4,"j");
map.put(5,"a");
map.put(6,"v");
LoadingCache<Integer, String> loadingCache = createGuavaCache();
// 此时缓存的过期时间为3s
for(int i = 1; i <= 6; i++){
System.out.println(loadingCache.get(i));
if (i>=3){
Thread.sleep(1000);
}
}
}
运行结果:
问题: 如果对缓存设置过期时间,在高并发下同时执行get操作,而此时缓存值已过期了,如果没有保护措施,则会导致大量线程同时调用生成缓存值的方法,比如从数据库读取,对数据库造成压力,这也就是我们常说的“缓存击穿”。
refreshAfterWrite: 当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。这两个配置的区别前者记录写入时间,后者记录写入或访问时间,内部分别用writeQueue和accessQueue维护。
默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。假如在同步监听模式下,监听方法中的逻辑特别复杂,执行效率慢,那此时如果有大量的key进行清理,会使整个缓存性能变得很低下,所以此时适合用异步监听RemovalListeners.asynchronous(RemovalListener, Executor)把监听器装饰为异步操作,移除key与监听key的移除分属2个线程。
// 设置缓存的移除通知(同步)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause());
}
})
// 设置缓存的移除通知(异步)
.removalListener(RemovalListeners.asynchronous(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
}
},Executors.newSingleThreadExecutor()))