1. 概述
1.1 简介
Guava缓存,谷歌开源的一种本地缓存,使用本节点的内存来存储的,实现原理类似于ConcurrentHashMap,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
1.2 本地缓存&分布式缓存
- 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较为合适;同时,它的缺点也是因为缓存跟应用程序耦合,多个应用程序无法直接共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
- 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接共享缓存。
2. 创建方式
2.1 CacheLoader的方式
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));
}
}
运行结果:
2.2 callable方式
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);
}
运行结果:
3. 缓存清除策略
3.1 基于容量的清除策略
清除策略:超过最大容量之后就会按照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));
}
}
运行结果:
3.2 基于权重的清除策略
使用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);;
}
});
3.3 基于存活时间的清除策略
- expireAfterWrite :写缓存后多久过期
- expireAfterAccess :读写缓存后多久过期
- refreshAfterWrite :写入数据后多久过期,只阻塞当前数据加载线程,其他线程返回旧值
expireAfterWrite与refreshAfterWrite混合使用情况:
- 当没有数据的时候,其他线程在加载数据的时候,当前线程会一直阻塞等待其他线程加载数据完成; 如果有数据的情况下其他线程正在加载数据,已经超过refreshAfterWrite设置时间但是没有超过expireAfterWrite设置的时间时当前线程返回旧数据。
- 如果有数据的情况下其他线程正在加载数据,已经超过expireAfterWrite设置的时间时当前线程阻塞等待其他线程加载数据完成. 这种情况适合与设置一个加载缓冲区的情况,既能保证过期后加载数据,又能保证长时间没访问多个线程并发时获取到过期旧数据的情况。
例:
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维护。
3.4 显式清除
- 清除单个key:Cache.invalidate(key)
- 批量清除key:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
3.5 基于引用的清除策略
- CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收
- CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收
- CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定。
4. 监听
默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。假如在同步监听模式下,监听方法中的逻辑特别复杂,执行效率慢,那此时如果有大量的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()))
5. 统计
- hitRate():缓存命中率
- hitMiss():缓存失误率
- loadcount() : 加载次数
- averageLoadPenalty():加载新值的平均时间,单位为纳秒
- evictionCount():缓存项被回收的总数,不包括显式清除
6. 常用API
- V getIfPresent(Object key): 获取缓存中key对应的value,如果缓存没命中,返回null
- V get(K key) :获取key对应的value,若缓存中没有,则调用LocalCache的load方法,从数据源中加载,并缓存
- void put(K key, V value) :如果缓存有值,覆盖,否则,新增
- void putAll(Map m):循环调用单个的方法
- void invalidate(Object key): 删除缓存
- void invalidateAll():清楚所有的缓存,相当远map的clear操作
- long size():获取缓存中元素的大概个数。注:元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素
- CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等
- asMap():获得缓存数据的ConcurrentMap快照
- cleanUp():清空缓存
- refresh(Key) :刷新缓存,即重新取缓存数据,更新缓存
- ImmutableMap getAllPresent(Iterable keys) :一次获得多个键的缓存值