前言
介绍了Caffeine和其他缓存框架的性能对比、Caffeine特性中的加载策略和回收策略,接下来我们继续实践Caffeine的其他特性。
最佳实践
1. 刷新策略
刷新策略可以通过LoadingCache.refresh(K)方法,异步为key对应的缓存元素刷新一个新的值。与回收策略不同的是,在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。
public static void refreshLoad() throws InterruptedException {
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
//设置在写入或者更新之后1分钟后,调用 CacheLoader 重新加载
.refreshAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, String>() {
@Override
public String load(Integer key) throws Exception {
String values = queryData(key);
log("load刷新,key:" + key + ",查询的数据库值:" + values);
return values;
}
@Nullable
@Override
public String reload(@NonNull Integer key, @NonNull String oldValue) throws Exception {
String values = queryData(key);
log("reload刷新,key:" + key + ",旧值:" + oldValue + ",查询的数据库值:" + values);
return values;
}
});
Thread thread1 = startLoadingCacheQuery("client1", cache);
Thread thread2 = startLoadingCacheQuery("client2", cache);
Thread.sleep(2000);
Thread thread3 = startLoadingCacheQuery("client3", cache);
Thread.sleep(1000);
Thread thread4 = startLoadingCacheQuery("client4", cache);
}
private static Thread startLoadingCacheQuery(String clientName, LoadingCache<Integer, String> cache) {
Thread thread = new Thread(() -> {
log("异步从缓存中查询数据开始");
String values = cache.get(1);
log("查询的key为:" + 1 + ",值为:" + values);
log("异步从缓存中查询数据结束");
});
thread.setName(clientName);
thread.start();
return thread;
}
private static String queryData(Integer key) throws InterruptedException {
String value = System.currentTimeMillis() + "";
return value;
}
private static void log(String msg) {
System.out.println(String.format("当前时间:%d,线程名称:%s,msg:%s", System.currentTimeMillis(), Thread.currentThread().getName(), msg));
}
2. 缓存写入传播
CacheWriter给缓存提供了充当底层资源的门面的能力,当其与CacheLoader一起使用的时候,所有的读和写操作都可以通过缓存向下传播。Writers提供了原子性的操作,包括从外部资源同步的场景。这意味着在缓存中,当一个key的写入操作在完成之前,后续其他写操作都是阻塞的,同时在这段时间内,尝试获取这个key对应的缓存元素的时候获取到的也将都是旧值。如果写入失败那么之前的旧值将会被保留同时异常将会被传播给调用者。
public static void write() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
// .expireAfterWrite(1, TimeUnit.SECONDS)
.writer(new CacheWriter<String, String>() {
@SneakyThrows
@Override
public void write(String key, String graph) {
// 持久化或者次级缓存
Thread.sleep(2000);
System.out.println(String.format("写入时间:%d,key:%s,value:%s", System.currentTimeMillis(), key, graph));
}
@Override
public void delete(String key, String value, RemovalCause cause) {
// 从持久化或者次级缓存中删除
System.out.println(String.format("delete事件,key:%s,value:%s,原因:%s:",key,value,cause.toString()));
}
})
.removalListener(new RemovalListener<String, String>() {
@Override
public void onRemoval(@Nullable String key, @Nullable String value, @NonNull RemovalCause removalCause) {
System.out.println(String.format("remove事件,key:%s,value:%s,原因:%s:",key,value,removalCause.toString()));
}
})
.build();
cache.put("name", "小明");
System.out.println(String.format("时间:%d,name:%s",System.currentTimeMillis(),cache.getIfPresent("name")));
//在这里获取name会同步等待"小明"写入完成
cache.put("name", "小强");
System.out.println(String.format("时间:%d,name:%s",System.currentTimeMillis(),cache.getIfPresent("name")));
Thread.sleep(2000);
System.out.println(String.format("时间:%d,name:%s",System.currentTimeMillis(),cache.getIfPresent("name")));
}
3. 统计
通过使用Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个CacheStats对象,其将会含有一些统计指标,比如:
hitRate(): 查询缓存的命中率
evictionCount(): 被驱逐的缓存数量
averageLoadPenalty(): 新值被载入的平均耗时
public static void statistics() throws InterruptedException {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10)
.recordStats()
.build(key->"小明");
int i=0;
while(i<1000){
i++;
cache.put("name"+i,"小明");
}
Thread.sleep(10000);
//缓存命中率
System.out.println("缓存命中率:"+cache.stats().hitRate());
//回收的缓存数量
System.out.println("回收的缓存数量:"+cache.stats().evictionCount());
//新值被载入的平均耗时
System.out.println("新值被载入的平均耗时:"+cache.stats().averageLoadPenalty());
}