本地缓存拆解
前言
Github:https://github.com/HealerJean
一、本地缓存
1、JVM
缓存
` JVM
缓存,也可以认为是堆缓存。其实就是创建一些全局变量,如
Map、List` 之类的容器用于存放数据
缺点:
①、只能显式的写入,清除数据。
②、不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO
等。
③、没有清除数据时的回调通知。
④、其他一些定制功能等。
2、Ehcache
、Guava Cache
由于
JVM
缓存的缺点,出现了一些专门用作JVM
缓存的开源工具出现了,如:Guava Cache
。它具有上文JVM
缓存不具有的功能,如自动清除数据、多种清除算法、清除回调等。
缺点:因为有了这些功能,所以必然会多出许多东西需要额外维护,自然也就增加了系统的消耗。
3、分布式缓存
上面的两种缓存其实都是堆内缓存,只能在单个节点中使用,这样在分布式场景下就招架不住了。于是也有了一些缓存中间件,如
Redis
、Memcached
,在分布式环境下可以共享内存。
二、Guava Cache
在高并发场景下我们很多场景都会使用缓存来抗高并发。通常的做法都是使用
redis
等分布式缓存+本地二级缓存方案来解决。即首先读取分布式缓存,如果分布式缓存挂了,则降级读取作为二级缓存的本地缓存。在实现本地缓存的时候,我们通常也不会闭门造车,而是使用Guava
Cache
1、创建缓存
1)concurrencyLevel
缓存的并发级别
并发级别是指可以同时写缓存的线程数
Guava
提供了设置并发级别的api
,使得缓存支持并发的写入和读取。同ConcurrentHashMap
类似Guava cache
的并发也是通过分离锁实现。在一般情况下,将并发级别设置为服务器cpu
核心数是一个比较不错的选择。
cache = CacheBuilder.newBuilder()
.concurrencyLevel(8)
2)缓存的初始容量设置
我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于
Guava
的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。
cache = CacheBuilder.newBuilder()
.initialCapacity(10)
2、驱逐策略
1)基于容量
超过缓存最大容量之后就会按照LRU最近虽少使用算法来移除缓存项
Guava Cache
可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当Cache
中的记录数量达到最大值后再调用put
方法向其中添加对象,Guava
会先从当前缓存的对象记录中选择一条删除掉,腾出空间后再将新的对象存储到Cache
中。⬤ 基于容量的清除:通过
CacheBuilder.maximumSize(long)
方法可以设置Cache
的最大容量数,当缓存数量达到或接近该最大值时,Cache
将清除掉那些最近最少使用的缓存;⬤ 基于权重的清除: 使用
CacheBuilder.weigher(Weigher)
指定一个权重函数,并且用CacheBuilder.maximumWeight(long)
指定最大总重。
cache = CacheBuilder.newBuilder()
.maximumSize(100)
2)基于时间
a、expireAfterAccess
缓存项在给定时间内没有被读/写访问,则回收
b、expireAfterWrite
设置写缓存后
n
秒钟过期,过期删除使用了
expireAfterWrites
之后,每次缓存失效LoadingCache
都会去调用我们实现的load
方法去重新加载缓存,在加载期间,所有线程对该缓存key
的访问都将被block
。所以如果实际加载缓存需要较长时间的话,这种方式不太适用。工作原理:这里
Guava
内部会对某个时间点失效的缓存做统一失效,即:只要有get
访问任一key
,就会失效当前时间失效的缓存,会移除当前key
。
cache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Optional<CacheDTO>>() {
@Override
public Optional<CacheDTO> load(Integer id) throws Exception {
log.info("线程名:{}, 加载数据开始", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(8);
Random random = new Random();
CacheDTO cacheDTO = new CacheDTO()
.setId(random.nextInt())
.setName("HealerJean");
log.info("线程名:{}, 加载数据结束", Thread.currentThread().getName());
return Optional.ofNullable(cacheDTO);
}
});
3、如何刷新
1)refreshAfterWrite
定时刷新,可以为缓存增加自动定时刷新功能。缓存项只有在被检索时才会真正刷新,即只有刷新间隔时间到了再去
get(key)
才会重新去执行reload
,否则就算刷新间隔时间到了也不会执行loading
操作。
实现:用了refreshAfterWrites
之后,需要自己实现 CacheLoader
的 reload
方法,在方法中创建一个 ListenableFutureTask
,然后将这个task
提交给线程池去异步执行,reload
方法最后返回这个 ListenableFutureTask
。这样做之后,缓存失效后重新加载就变成了异步。加载期间尝试获取缓存的线程也都不会被block
,而是获取到加载之前的值。当加载完毕之后,各个线程就能取到最新值了(也就是说:只阻塞当前数据加载线程,其他线程返回旧值)。
场景:对于互联网高并发的场景,refreshAfterWrites
这种使用异步刷新缓存的方法应该是我们首先考虑的,取到一个过期的旧值总比大量线程全都被 block
要好。expireAfterWrite
可能会导致请求大量堆积,连接数不够等一些列问题。
expireAfterWrite
与 refreshAfterWrite
同时配置的话。
⬤
expire
小于等于refresh
时间,优先expire
失效,同时满足走expire
(无法refresh
)。⬤
expire
大于refresh
时间,优先refresh
,同时满足走expire
。
private static Executor executor = Executors.newFixedThreadPool(10);
cache = CacheBuilder.newBuilder()
.refreshAfterWrite(7, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Optional<CacheDTO>>() {
@Override
public Optional<CacheDTO> load(Integer id) throws Exception {
return Optional.ofNullable(null);
}
@Override
public ListenableFuture<Optional<CacheDTO>> reload(Integer id, Optional<CacheDTO> oldValu)
throws Exception {
log.info("线程名:{}, reload 开始", Thread.currentThread().getName());
ListenableFutureTask<Optional<CacheDTO>> futureTask = ListenableFutureTask.create(() -> {
Random random = new Random();
CacheDTO cacheDTO = new CacheDTO()
.setId(random.nextInt())
.setName("HealerJean");
return Optional.ofNullable(cacheDTO);
});
executor.execute(futureTask);
log.info("线程名:{}, reload 已执行", Thread.currentThread().getName());
return futureTask;
}
});
4、统计
是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
cache = CacheBuilder.newBuilder()
.recordStats()
log.info("缓存统计信息:{}", cache.stats());
5、监听器
1)removalListener
设置缓存的移除通知
cache = CacheBuilder.newBuilder()
.removalListener(notification -> {
log.info("缓存移除通知:key:{}, value{}, case:{}",
notification.getKey(),
notification.getValue(),
notification.getCause());
})
6、cache#get
Cache
的get
方法有两个参数,第一个参数是要从Cache
中获取记录的key
,第二个记录是一个Callable
对象。⬤ 当缓存中已经存在
key
对应的记录时,get
方法直接返回key
对应的记录。⬤ 如果缓存中不包含
key
对应的记录,Guava
会启动一个线程执行Callable
对象中的call
方法,call
方法的返回值会作为key
对应的值被存储到缓存中,并且被get
方法返回。
CacheDTO cacheDTO = cache.get(1).orElse(null);
CacheDTO cacheDTO = cache.get(0, () -> {
CacheDTO cacheDTO1 = new CacheDTO()
.setId(0)
.setName("NULL");
return Optional.ofNullable(cacheDTO1);
}).orElse(null);
7、工具类
1)测试类
@Slf4j
public class GuavaCacheService {
private static Executor executor = Executors.newFixedThreadPool(10);
private static LoadingCache<Integer, Optional<CacheDTO>> cache = null;
public static void initCache() {
log.info("线程名:{}, LoadingCache初始化", Thread.currentThread().getName());
cache = CacheBuilder.newBuilder()
//设置并发级别为8,
.concurrencyLevel(8)
//设置缓存容器的初始容量为10
.initialCapacity(10)
//设置最大存储
.maximumSize(100)
//是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 缓存清除策略
// expireAfterWrite 写缓存后多久过期
// expireAfterAccess 缓存项在给定时间内没有被读/写访问
.expireAfterWrite(5, TimeUnit.SECONDS)
// 定时刷新,只阻塞当前数据加载线程,其他线程返回旧值。
.refreshAfterWrite(7, TimeUnit.SECONDS)
//设置缓存的移除通知
.removalListener(notification -> {
log.info("缓存移除通知:key:{}, value{}, case:{}",
notification.getKey(),
notification.getValue(),
notification.getCause());
})
.build(new CacheLoader<Integer, Optional<CacheDTO>>() {
@Override
public Optional<CacheDTO> load(Integer id) throws Exception {
log.info("线程名:{}, load 开始", Thread.currentThread().getName());
CacheDTO cacheDTO = new CacheDTO()
.setId(1)
.setName("HealerJean");
log.info("线程名:{}, load 结束", Thread.currentThread().getName());
return Optional.ofNullable(cacheDTO);
}
@Override
public ListenableFuture<Optional<CacheDTO>> reload(Integer id,
Optional<CacheDTO> oldValu) throws Exception {
log.info("线程名:{}, reload 开始", Thread.currentThread().getName());
ListenableFutureTask<Optional<CacheDTO>> futureTask = ListenableFutureTask.create(() -> {
Random random = new Random();
CacheDTO cacheDTO = new CacheDTO()
.setId(random.nextInt())
.setName("HealerJean");
return Optional.ofNullable(cacheDTO);
});
executor.execute(futureTask);
log.info("线程名:{}, reload 已执行", Thread.currentThread().getName());
return futureTask;
}
});
log.info("线程名:{}, LoadingCache 结束", Thread.currentThread().getName());
}
public static void main(String[] args) {
initCache();
//模拟线程并发
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
CacheDTO cacheDTO = cache.get(1).orElse(null);
log.info("main1 线程名:{} 时间:{} cache:{}",
Thread.currentThread().getName(),
LocalDateTime.now(), cacheDTO);
TimeUnit.SECONDS.sleep(3);
}
} catch (Exception ignored) {
}
}).start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
CacheDTO cacheDTO = cache.get(0, () -> {
CacheDTO cacheDTO1 = new CacheDTO()
.setId(0)
.setName("NULL");
return Optional.ofNullable(cacheDTO1);
}).orElse(null);
log.info("main2 线程名:{} 时间:{} cache:{}",
Thread.currentThread().getName(),
LocalDateTime.now(),
cacheDTO);
TimeUnit.SECONDS.sleep(5);
}
} catch (Exception ignored) {
}
}).start();
log.info("缓存统计信息:{}", cache.stats());
}
}
3)工具类
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CacheManager {
/**
* 缓存项最大数量
*/
private static final long GUAVA_CACHE_SIZE = 100000;
/**
* 缓存时间:天
*/
private static final long GUAVA_CACHE_DAY = 10;
/**
* 缓存操作对象
*/
private static LoadingCache<Long, String> GLOBAL_CACHE = null;
static {
try {
GLOBAL_CACHE = loadCache(new CacheLoader<Long, String>() {
@Override
public String load(Long key) throws Exception {
// 处理缓存键不存在缓存值时的处理逻辑
LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
//缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
.maximumSize(GUAVA_CACHE_SIZE)
// 设置缓存在写入之后 设定时间 后失效
.expireAfterWrite(GUAVA_CACHE_DAY, TimeUnit.DAYS)
//移除监听器,缓存项被移除时会触发
.removalListener((RemovalListener<Long, String>) rn -> {
//逻辑操作
})
//开启Guava Cache的统计功能
.recordStats()
.build(new CacheLoader<Long, String>() {
@Override
public String load(Long aLong) throws Exception {
// 处理缓存键不存在缓存值时的处理逻辑
return "null";
}
});
}
});
} catch (Exception e) {
log.error("初始化Guava Cache出错", e);
}
}
/**
* 设置缓存值
* 注: 若已有该key值,则会先移除(会触发removalListener移除监听器),再添加
*
* @param key
* @param value
*/
public static void put(Long key, String value) {
try {
GLOBAL_CACHE.put(key, value);
} catch (Exception e) {
log.error("设置缓存值出错", e);
}
}
/**
* 批量设置缓存值
*
* @param map
*/
public static void putAll(Map<? extends Long, ? extends String> map) {
try {
GLOBAL_CACHE.putAll(map);
} catch (Exception e) {
log.error("批量设置缓存值出错", e);
}
}
/**
* 获取缓存值
* 注:如果键不存在值,将调用CacheLoader的load方法加载新值到该键中
*
* @param key
* @return
*/
public static String get(Long key) {
String token = "";
try {
token = GLOBAL_CACHE.get(key);
} catch (Exception e) {
log.error("获取缓存值出错", e);
}
return token;
}
/**
* 移除缓存
*
* @param key
*/
public static void remove(Long key) {
try {
GLOBAL_CACHE.invalidate(key);
} catch (Exception e) {
log.error("移除缓存出错", e);
}
}
/**
* 批量移除缓存
*
* @param keys
*/
public static void removeAll(Iterable<Long> keys) {
try {
GLOBAL_CACHE.invalidateAll(keys);
} catch (Exception e) {
log.error("批量移除缓存出错", e);
}
}
/**
* 清空所有缓存
*/
public static void removeAll() {
try {
GLOBAL_CACHE.invalidateAll();
} catch (Exception e) {
log.error("清空所有缓存出错", e);
}
}
/**
* 获取缓存项数量
*
* @return
*/
public static long size() {
long size = 0;
try {
size = GLOBAL_CACHE.size();
} catch (Exception e) {
log.error("获取缓存项数量出错", e);
}
return size;
}
}
8、原理
1)如何做到高效读写
Guava Cache
借鉴了ConcurrentHashMap
的实现原理 ( 基于1.7版本的实现,即没有使用红黑树),使用了桶+链表的方式来实现。其这部分实现代码逻辑集合都和ConcurrentHashMap
一样。如下是新增缓存项的一段代码,是不是很ConcurrentHashMap
很像呢。
Guava Cache
借鉴了ConcurrentHashMap
的思想(甚至代码结构和实现都差不多),通过分段锁的方式解决了高并发读写的问题。
public V put(K key, V value) {
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(value);
int hash = this.hash(key);
return this.segmentFor(hash).put(key, hash, value, false);
}
2)缓存项数量和容量大小限制实现
maximumWeight
和 这二者的实现都是通过权重来实现的。实现容量大小限制的时候,通过
maximumWeight
来置总容量大小,然后通过weigher
函数来计算并告诉Guava Cache
每个缓存项的容量大小。这样Guava Cache
就只需要将所有的缓存项目的权重值相加就能够知道其是否超过最大容量限制了。在实现缓存项数据量大小限制的时候,虽然是通过
maximumSize
来指定的最大缓存项数据量。其实底层使用了和权重相同的代码逻辑实现,只是这里每个缓存项的权重为1。
三、Caffeine Cache
1、缓存比较
缓存 | 说明 |
---|---|
Cache |
基本缓存,不支持自动加载和刷新,需要手动管理缓存的过期时间和缓存数据的移除。 |
AsyncCache |
异步缓存,与AsyncLoadingCache 类似,但是它不支持数据加载功能(当缓存中没有请求的数据时,自动加载数据)。 |
LoadingCache |
同步缓存,支持自动加载和刷新,当缓存中没有请求的数据时,会阻塞直到数据加载完成并返回结果。 |
AsyncLoadingCache |
异步缓存,支持自动加载和刷新,当缓存中没有请求的数据时,会立即返回一个ListenableFuture 对象,当数据加载完成后,会回调相关的监听器并返回结果。 |
2、Caffeine
使用
1)Cache
基本缓存,不支持自动加载和刷新,需要手动管理缓存的过期时间和缓存数据的移除。
说明
Cache
接口提供了显式搜索查找、更新和移除缓存元素的能力。当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get
也许会返回null
。
@Test
public void test() {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
String key = "KeyName";
// 1、查找一个缓存元素, 没有查找到的时候返回null
String value = cache.getIfPresent(key);
// 2、查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
value = cache.get(key, k -> createExpensiveValue(key));
// 3、添加或者更新一个缓存元素
cache.put(key, value);
// 4、移除一个缓存元素
cache.invalidate(key);
}
/**
* createExpensiveValue
*
* @param key key
* @return String
*/
private String createExpensiveValue(String key) {
return RandomUtils.nextInt() + key;
}
2)AsyncCache
异步缓存,与
AsyncLoadingCache
类似,但是它不支持数据加载功能(当缓存中没有请求的数据时,自动加载数据)。1、
AsyncCache
就是Cache
的异步形式,提供了Executor
生成缓存元素并返回CompletableFuture
的能力。2、默认的线程池实现是
ForkJoinPool.commonPool()
,当然你也可以通过覆盖并实现Caffeine.executor(Executor)
方法来自定义你的线程池选择
@Test
public void asyncCache() {
AsyncCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
String key = "KeyName";
// 1、查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<String> graph = cache.getIfPresent(key);
// 2、查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveValue(key));
// 3、添加或者更新一个缓存元素
cache.put(key, graph);
// 4、移除一个缓存元素
cache.synchronous().invalidate(key);
}
3)LoadingCache
同步缓存,支持自动加载和刷新,当缓存中没有请求的数据时,会阻塞直到数据加载完成并返回结果。
说明:一个
LoadingCache
是一个Cache
附加上CacheLoader
能力之后的缓存实现。如果缓存不存在在,则会通过CacheLoader.load
来生成对应的缓存元素。
@Test
public void loadingCache() {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::createExpensiveValue);
// 批量查找缓存,如果缓存不存在则生成缓存元素
List<String> keys = Lists.newArrayList("key1", "key2");
Map<String, String> valueMap = cache.getAll(keys);
cache.asMap().get("key");
String key = cache.get("key");
}
3)AsyncLoadingCache
异步缓存,支持自动加载和刷新,当缓存中没有请求的数据时,会立即返回一 个
ListenableFuture
对象,当数据加载完成后,会回调相关的监听器并返回结果。
@Test
public void asyncLoadingCache() {
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 你可以选择: 去异步的封装一段同步操作来生成缓存元素
.buildAsync(this::createExpensiveValue);
// 你也可以选择: 构建一个异步缓存元素操作并返回一个future
// .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 1、查找缓存元素,如果其不存在,将会异步进行生成
String key = "KeyName";
CompletableFuture<String> graph = cache.get(key);
// 2、批量查找缓存元素,如果其不存在,将会异步进行生成
List<String> keys = Lists.newArrayList("key1", "key2");
CompletableFuture<Map<String, String>> graphs = cache.getAll(keys);
}
2、驱逐策略
1)基于容量
a、基于缓存内的元素个数进行驱逐
// 基于缓存内的元素个数进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));
b、基于缓存内元素权重进行驱逐
// 基于缓存内元素权重进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));
2)基于时间
过期策略 | 说明 |
---|---|
expireAfterWrite |
在写入缓存后的一段时间后过期,即从写入时开始计时,过期时间到达后缓存数据会被自动移除。如果你需要在缓存项被写入一段时间后过期,则可以使用expireAfterWrite |
expireAfterRead |
在缓存项自最近一次访问之后经过一段时间后过期,即从最近一次访问开始计时,过期时间到达后缓存数据会被自动移除。如果你需要在缓存项自最近一次访问之后过期,则可以使用expireAfterRead |
expireAfterUpdate |
在缓存项自最近一次更新之后经过一段时间后过期,即从最近一次更新开始计时,过期时间到达后缓存数据会被自动移除。如果你需要在缓存项自最近一次更新之后过期,则可以使用expireAfterUpdate 。 |
expireAfterAccess |
在缓存项自最近一次访问之后经过一段时间后过期,即从最近一次访问开始计时,过期时间到达后缓存数据会被自动移除。与expireAfterRead 方法不同的是,expireAfterAccess 方法会在缓存项被访问一段时间后过期,而不管缓存项是否被读取。例如,如果我们设置了一个缓存项在5分钟内过期,而这个缓存项在4分钟内被多次访问但并没有被读取,那么在5分钟后这个缓存项仍然会被移除。如果你需要在缓存项被访问一段时间后过期,则可以使用expireAfterAccess |
3)基于引用
a、当 key
和 缓存元素 都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));
b、当进行GC的时候进行驱逐
// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));
3、刷新机制
3.1、缓存比较
缓存 | 说明 |
---|---|
Cache |
Cache 中没有内置的刷新机制,但是我们可以通过定时任务或者手动调用Cache.put(key, value) 方法来刷新缓存项的值。 |
AsyncCache |
AsyncCache 没有内置的刷新机制,但是我们可以通过定时任务或者手动调用AsyncCache.put(key, value) 方法来刷新缓存项的值。 |
LoadingCache |
LoadingCache 中的刷新策略可以帮助我们在缓存项过期之前,同步地刷新缓存项的值。当缓存项过期时,Guava 缓存库会自动调用 CacheLoader.reload 方法来同步地刷新缓存项的值。 |
AsyncLoadingCache |
继承自AsyncCache , AsyncLoadingCache 中的刷新策略可以帮助我们在缓存项过期之前,异步地刷新缓存项的值。当缓存项过期时,Guava 缓存库会自动调用AsyncCacheLoader.reload 方法来异步地刷新缓存项的值。 |
3.2、如何刷新
刷新动作 | 说明 |
---|---|
被动刷新 | 这是 Caffeine 默认的刷新机制。当缓存中的数据过期后,再次访问该数据时,会触发一次异步刷新操作,更新缓存中的数据。这种刷新机制不需要额外的刷新操作,而是通过再次访问数据时自动触发刷新操作。 |
主动刷新: | 这种刷新机制会在缓存数据过期后,立即触发一次异步刷新操作,更新缓存中的数据。与被动刷新不同的是,主动刷新需要额外的刷新操作,而不是通过再次访问数据时自动触发刷新操作。 |
定时刷新: | 这种刷新机制会在指定的时间间隔内,定期触发异步刷新操作,更新缓存中的数据。可以通过设置 refreshAfterWrite 和 expireAfterWrite 时间来实现定时刷新。定时刷新的优点是可以避免缓存数据过期后仍然被访问的问题,同时也可以减少不必要的异步刷新操作。 |
4、统计
通过使用
Caffeine.recordStats()
方法可以打开数据收集功能。Cache.stats()
方法将会返回一个CacheStats
对象,其将会含有一些统计指标,比如:
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
| 方法 | 说明 |
| ———————- | ——————– |
| ` hitRate(): | 查询缓存的命中率 |
|
evictionCount(): | 被驱逐的缓存数量 |
|
averageLoadPenalty()` | 新值被载入的平均耗时 |