一、学习目标
1、掌握Caffeine的3种填充策略:
- 手动填充数据
- 同步加载
- 异步加载
2、掌握Caffeine提供了3种回收策略:
- 基于大小回收
- 基于时间回收
- 基于引用回收
3、Caffeine的移除监听器
如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。
删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。
4、Caffeine的CacheWriter
CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。 如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。
能监听到的动作:
- CacheWriter可以同步的监听到缓存的创建、变更和删除操作。
不能监听到的动作:
- 加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。
注意事项:
- CacheWriter不能与弱键或AsyncLoadingCache一起使用。
应用场景:
- 可以配合外部存储使用
5、Caffeine的统计
6、Caffeine的刷新
- 刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。
二、Caffeine简介
Caffeine是基于Java8的高性能缓存库,可提供接近最佳的命中率。Caffeine的底层使用了ConcurrentHashMap,支持按照一定的规则或者自定义的规则使缓存的数据过期,然后销毁。
Caffeine的特性:
- 自动把数据加载到本地缓存中,并且可以配置异步;
- 淘汰策略:基于数量剔除策略;基于失效时间剔除策略,这个时间是从最后一次操作算起【访问或者写入】;
- 异步刷新;
- Key会被包装成Weak引用;Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
- 数据剔除提醒;
- 写入广播机制;
- 缓存访问可以统计;
Caffeine的内部结构:
- Cache的内部包含着一个ConcurrentHashMap:这也是存放我们所有缓存数据的地方,众所周知,ConcurrentHashMap是一个并发安全的容器,这点很重要,可以说Caffeine其实就是一个被强化过的ConcurrentHashMap。
- Scheduler:定期清空数据的一个机制,可以不设置,如果不设置则不会主动的清空过期数据。
- Executor:指定运行异步任务时要使用的线程池。可以不设置,如果不设置则会使用默认的线程池,也就是ForkJoinPool.commonPool()。
三、Caffeine的3种填充策略
Caffeine提供了3种填充策略:手动填充数据,同步加载,异步加载。
1、手动填充数据:
public class CaffeineManualTest {
@Test
public void test() {
// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
Cache<Integer, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
int key1 = 1;
// getIfPresent(Object key):根据key从缓存中获取值,如果没有返回NULL
System.out.println(cache.getIfPresent(key1));
// get(key, Function):根据key查询一个缓存,如果没有将返回提供默认值,并保存到缓存
// 通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据,更加建议使用cache.get(key,k - > value) 的方式
System.out.println(cache.get(key1, new Function<Integer, String>() {
@Override
public String apply(Integer key) {
System.out.println(key);
return "abcd";
}
}));
System.out.println(cache.getIfPresent(key1));
// put(key, value):会覆盖缓原来key的数据
String value1 = "wxyz";
cache.put(key1, value1);
System.out.println(cache.getIfPresent(key1));
// invalidate(key):移除数据,让数据失效
cache.invalidate(key1);
System.out.println(cache.getIfPresent(key1));
}
}
如果同时有多个线程进行get,那么这个Function对象是否会被执行多次呢?
- 实际上不会的,可以从结构图看出,Caffeine内部最主要的数据结构就是一个ConcurrentHashMap,而get的过程最终执行的便是ConcurrentHashMap.compute,这里仅会被执行一次。
- get 方法是以阻塞方式执行调用,即使多个线程同时请求该值也只会调用一次Function方法。这样可以避免与其他线程的写入竞争,这也是为什么使用 get 优于 getIfPresent 的原因。
2、同步加载:
利用这个同步机制,也就是在CacheLoader对象中的load函数中,当从Caffeine缓存中取不到数据的时候,则从数据库中读取数据,将其插入缓存中。通过这个机制和数据库结合使用。
public class CaffeineLoadingTest {
/**
* 从数据库获取数据
*
* @param key
* @return
*/
private String getFromDataBase(int key) {
return key + RandomStringUtils.randomAlphabetic(8);
}
@Test
public void test() {
// 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build(
new CacheLoader<Integer, String>() {
/**
* 默认情况下,get、getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。
*
* @param key
* @return
*/
@Override
public String load(@NonNull Integer key) {
return getFromDataBase(key);
}
/**
* 在loadAll被重写的情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.loadAll方法来构建缓存的值。
*
* @param keys
* @return
*/
@Override
public @NonNull Map<Integer, String> loadAll(@NonNull Iterable<? extends Integer> keys) {
Map<Integer, String> map = new HashMap<>();
for (Integer key : keys) {
map.put(key, getFromDataBase(key) + "abcd");
}
return map;
}
}
);
int key1 = 100;
// get(key, Function):根据key查询一个缓存,如果没有将返回提供默认值,并保存到缓存
// 通过cache.put(key, value)方法显示的将数控放入缓存,但是这样子会覆盖缓原来key的数据,更加建议使用cache.get(key,k - > value) 的方式
String value1 = cache.get(key1);
System.out.println(value1);
// getAll(keys):批量查找
Map<Integer, String> dataMap = cache.getAll(Arrays.asList(1, 2, 3, 100));
System.out.println(dataMap);
}
}
运行结果:
100FDQHepbj
{1=1UwoFIuYoabcd, 2=2eDQKMwqpabcd, 3=3HYnJXUsRabcd, 100=100FDQHepbj}
3、异步加载:
public class CaffeineAsynchronousTest {
/**
* 从数据库获取数据
*
* @param key
* @return
*/
private String getFromDataBase(int key) {
System.out.println("2当前所在线程:" + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return key + RandomStringUtils.randomAlphabetic(8);
}
private CompletableFuture<String> getValue(Integer key) {
System.out.println("2当前所在线程:" + Thread.currentThread().getName());
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.supplyAsync(() -> key + RandomStringUtils.randomAlphabetic(8));
}
@Test
public void test2() throws ExecutionException, InterruptedException {
AsyncLoadingCache<Integer, String> asyncLoadingCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
// 使用executor设置线程池
// .executor(Executors.newSingleThreadExecutor())
.buildAsync(key -> getFromDataBase(key));
// .buildAsync(key -> getValue(key).get(1, TimeUnit.SECONDS));
Integer key = 1;
// put(key, value):会覆盖缓原来key的数据
// 此时不会执行setAsyncValue()、getFromDataBase()
String value1 = "wxyz1";
asyncLoadingCache.put(key, CompletableFuture.completedFuture(value1));
System.out.println("1当前所在线程:" + Thread.currentThread().getName());
CompletableFuture<String> future = asyncLoadingCache.get(key);
String value = future.get();
System.out.println(value);
}
}
四、Caffeine的3种回收策略
Caffeine提供了3种回收策略:基于大小回收,基于时间回收,基于引用回收。
1、基于大小回收:
基于最大缓存数量:
@Test
public void test() throws InterruptedException {
// 初始化缓存,设置了1分钟的写过期,2的缓存最大个数
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(2)
.build();
cache.put(1, "a");
System.out.println(cache.estimatedSize());
cache.put(2, "b");
System.out.println(cache.estimatedSize());
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2)));
// 触发缓存回收(异步)
cache.put(3, "c");
System.out.println(cache.estimatedSize());
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3)));
// 触发缓存回收(异步)
cache.put(4, "d");
System.out.println(cache.estimatedSize());
// 缓存回收是异步执行,cleanUp()可以等待异步执行完成
// cache.cleanUp();
Thread.sleep(1000);
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3, 4)));
}
运行结果:
1
2
{1=a, 2=b}
3
{1=a, 2=b, 3=c}
4
{2=b, 4=d}
基于权重:
/**
* 基于权重
*/
@Test
public void testMaximumWeight() {
Cache<Integer, Integer> cache = Caffeine.newBuilder()
// TODO 指定指定缓存最大权重值,配合weigher使用,随着缓存大小逐渐接近最大值,会回收最近或被很少使用的缓存,maximumWeight与maximumSize不能同时使用
.maximumWeight(11)
// 指定权重计算方式,缓存创建或更新时触发
.weigher(new Weigher<Object, Object>() {
@Override
public @NonNegative int weigh(@NonNull Object key, @NonNull Object value) {
if (value instanceof Integer) {
System.out.println("是Integer类型的: " + value);
return (Integer) value;
}
System.out.println("不是Integer类型的: " + value);
return 0;
}
})
.build();
List<Integer> keys = new ArrayList<>();
for (int i = 0; i < 100; i++) {
cache.put(i, i);
keys.add(i);
}
Map<Integer, Integer> map = cache.getAllPresent(keys);
map.forEach((k, v) -> System.out.println(k + ":" + v));
cache.cleanUp();
System.out.println();
map = cache.getAllPresent(keys);
map.forEach((k, v) -> System.out.println(k + ":" + v));
int sum = 0;
for (Integer ele : map.keySet()) {
sum += ele;
}
System.out.println("元素总和: " + sum );
// Thread.sleep(1000);
System.out.println(cache.estimatedSize());
}
运行结果:
是Integer类型的: 0
是Integer类型的: 1
是Integer类型的: 2
是Integer类型的: 3
...
是Integer类型的: 97
是Integer类型的: 98
是Integer类型的: 99
0:0
1:1
2:2
3:3
...
97:97
98:98
99:99
0:0
8:8
元素总和: 8
2
2、基于时间回收:
- expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
- expireAfterWrite(long, TimeUnit):在最后一次写入缓存后开始计时,在指定的时间后过期。
- expireAfter(Expiry):自定义策略,过期时间由Expiry实现独自计算。
注意事项:
- expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。
- 缓存的删除策略使用的是惰性删除和定时删除。这两个删除策略的时间复杂度都是O(1)。
public class CaffeineEvictionPolicyBaseOnTimeTest {
/**
* 从数据库获取数据
*
* @param key
* @return
*/
private String getFromDataBase(int key) {
return key + RandomStringUtils.randomAlphabetic(8);
}
/**
* 到期策略
*
* @throws InterruptedException
*/
@Test
public void testExpirePolicy() throws InterruptedException {
// 基于固定的到期策略进行回收
LoadingCache<Integer, Object> cache = Caffeine.newBuilder()
// TODO 在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> getFromDataBase(key));
LoadingCache<Integer, Object> cache1 = Caffeine.newBuilder()
// TODO 在最后一次写入缓存后开始计时,在指定的时间后过期。
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> getFromDataBase(key));
}
/**
* 自定义到期策略
*/
@Test
public void testCustomerExpirePolicy() {
LoadingCache<Integer, Object> cache2 = Caffeine.newBuilder()
.expireAfter(new Expiry<Object, Object>() {
@Override
public long expireAfterCreate(@NonNull Object key, @NonNull Object value, long currentTime) {
return 0;
}
@Override
public long expireAfterUpdate(@NonNull Object key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
return 0;
}
@Override
public long expireAfterRead(@NonNull Object key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) {
return 0;
}
}).build(key -> getFromDataBase(key));
}
}
3、基于引用回收:
- weakKeys(): 使用弱引用存储key。如果没有其他地方对该key有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。
- weakValues() :使用弱引用存储value。如果没有其他地方对该value有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。
- softValues() :使用软引用存储value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。 softValues() 将使用身份相等(identity) (==) 而不是equals() 来比较值。
注意事项:
- AsyncLoadingCache不支持弱引用和软引用
- Caffeine.weakValues()和Caffeine.softValues()不可以一起使用
- 使用到的回收策略时LRU算法
public class CaffeineEvictionPolicyBaseOnReferenceTest {
/**
* 从数据库获取数据
*
* @param key
* @return
*/
private String getFromDataBase(int key) {
return key + RandomStringUtils.randomAlphabetic(8);
}
@Test
public void test() {
LoadingCache<Integer, String> cache1 = Caffeine.newBuilder()
// 当key和value都没有引用时(都为null)驱逐缓存
.weakKeys()
.weakValues()
.build(key -> getFromDataBase(key));
LoadingCache<Integer, String> cache2 = Caffeine.newBuilder()
// 当垃圾收集器需要释放内存时驱逐
.softValues()
.build(key -> getFromDataBase(key));
}
}
五、Caffeine的移除监听器
如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。
删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。
public class CaffeineRemovalTest {
@Test
public void test() throws InterruptedException {
Cache<Integer, String> cache = Caffeine.newBuilder()
// TODO 指定最大缓存数量,如果超过这个值,则会剔除很久没有被访问过或者不经常使用的缓存
.maximumSize(2)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
System.out.println("【日志打印】触发了 " + cause.name() + " 策略, 清除的key = " + key + ", value=" + value);
}
})
.build();
cache.put(1, "a");
System.out.println(cache.estimatedSize());
cache.put(2, "b");
System.out.println(cache.estimatedSize());
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2)));
// 触发缓存回收(异步)
cache.put(3, "c");
System.out.println(cache.estimatedSize());
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3)));
// 触发缓存回收(异步)
cache.put(4, "d");
System.out.println(cache.estimatedSize());
// 缓存回收是异步执行,cleanUp()可以等待异步执行完成
// cache.cleanUp();
Thread.sleep(1000);
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3, 4)));
cache.put(5, "e");
// 手动删除缓存
cache.invalidate(5);
System.out.println(cache.getAllPresent(Lists.newArrayList(1, 2, 3, 4)));
}
}
运行结果:
1
2
{1=a, 2=b}
3
{1=a, 2=b, 3=c}
4
【日志打印】触发了 SIZE 策略, 清除的key = 1, value=a
【日志打印】触发了 SIZE 策略, 清除的key = 3, value=c
{2=b, 4=d}
【日志打印】触发了 EXPLICIT 策略, 清除的key = 5, value=e
【日志打印】触发了 SIZE 策略, 清除的key = 4, value=d
{2=b}
六、Caffeine的CacheWriter
CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。 如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。
能监听到的动作:
- CacheWriter可以同步的监听到缓存的创建、变更和删除操作。
不能监听到的动作:
- 加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。
注意事项:
- CacheWriter不能与弱键或AsyncLoadingCache一起使用。
应用场景:
- 可以配合外部存储使用
public class CaffeineWriterTest {
/**
* 充当二级缓存用,生命周期仅活到下个gc
*/
private Map<Integer, WeakReference<Integer>> secondCacheMap = new ConcurrentHashMap<>();
@Test
public void test() throws InterruptedException {
LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
.maximumSize(1)
.writer(
new CacheWriter<Integer, Integer>() {
/**
* 写入到外部存储
*
* @param key
* @param value
*/
@Override
public void write(@NonNull Integer key, @NonNull Integer value) {
secondCacheMap.put(key, new WeakReference<>(value));
System.out.println("写入到外部存储,将key = " + key + "放入二级缓存中");
System.out.println("二级缓存: " + secondCacheMap);
System.out.println();
}
/**
* 删除外部存储
*
* @param key
* @param value
* @param cause
*/
@Override
public void delete(@NonNull Integer key, @Nullable Integer value, @NonNull RemovalCause cause) {
switch (cause) {
// 手动调用invalidate或remove等方法
case EXPLICIT:
secondCacheMap.remove(key);
System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
System.out.println("二级缓存: " + secondCacheMap);
System.out.println();
break;
// 超出缓存设定大小
case SIZE:
secondCacheMap.remove(key);
System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
System.out.println("二级缓存: " + secondCacheMap);
System.out.println();
break;
// 调用put等方法进行修改
case REPLACED:
secondCacheMap.remove(key);
System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
System.out.println("二级缓存: " + secondCacheMap);
System.out.println();
break;
// 设置了key或value的引用方式
case COLLECTED:
secondCacheMap.remove(key);
System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
System.out.println("二级缓存: " + secondCacheMap);
System.out.println();
break;
// 设置了过期时间
case EXPIRED:
secondCacheMap.remove(key);
System.out.println("删除外部存储, " + cause.name() + " 策略, 将key = " + key);
System.out.println("二级缓存: " + secondCacheMap);
System.out.println();
break;
default:
break;
}
}
}
)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
System.out.println("【日志打印】触发了 " + cause.name() + " 策略, 清除的key = " + key + ", value=" + value);
}
})
.build(new CacheLoader<Integer, Integer>() {
/**
* 默认情况下,get、getAll将会对缓存中没有值的key分别调用CacheLoader.load方法来构建缓存的值。
*
* @param key
* @return
*/
@Nullable
@Override
public Integer load(@NonNull Integer key) {
WeakReference<Integer> value = secondCacheMap.get(key);
if (value == null) {
return 100;
}
System.out.println("触发CacheLoader#load(),从二级缓存读取key = " + key);
System.out.println();
return value.get();
}
}
);
cache.put(1, 1);
cache.put(2, 2);
// 缓存回收是异步执行,cleanUp()可以等待异步执行完成
// cache.cleanUp();
Thread.sleep(1000);
// 根据key查询一个缓存,如果没有将返回提供默认值,并保存到缓存,又超过了缓存最大值,触发清理操作
System.out.println(cache.get(1));
Thread.sleep(1000);
System.out.println(cache.getIfPresent(2));
}
}
运行结果:
写入到外部存储,将key = 1放入二级缓存中
二级缓存: {1=java.lang.ref.WeakReference@47d384ee}
写入到外部存储,将key = 2放入二级缓存中
二级缓存: {1=java.lang.ref.WeakReference@47d384ee, 2=java.lang.ref.WeakReference@1ff8b8f}
删除外部存储, SIZE 策略, 将key = 1
二级缓存: {2=java.lang.ref.WeakReference@1ff8b8f}
【日志打印】触发了 SIZE 策略, 清除的key = 1, value=1
100
删除外部存储, SIZE 策略, 将key = 2
二级缓存: {}
【日志打印】触发了 SIZE 策略, 清除的key = 2, value=2
null
六、Caffeine的统计
public class CaffeineStatsTest {
@Test
public void test() {
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(10_00)
.recordStats()
.build();
for (int i = 0; i < 100; i++) {
cache.get(i, new Function<Integer, String>() {
@Override
public String apply(Integer integer) {
return RandomStringUtils.randomAlphanumeric(8);
}
});
}
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
System.out.println(cache.getIfPresent(i));
System.out.println();
}
}
System.out.println("加载新值所花费的平均时间 totalLoadTime / (loadSuccessCount + loadFailureCount): " + cache.stats().averageLoadPenalty());
System.out.println("缓存逐出的数量: " + cache.stats().evictionCount());
System.out.println("缓存逐出的数量(权重): " + cache.stats().evictionWeight());
System.out.println("返回命中缓存的总数: " + cache.stats().hitCount());
System.out.println("返回命中与请求的比率: " + cache.stats().hitRate());
System.out.println("加载新值的总次数: " + cache.stats().loadCount());
System.out.println("加载新值成功的总次数: " + cache.stats().loadSuccessCount());
System.out.println("加载新值失败的总次数: " + cache.stats().loadFailureCount());
System.out.println("加载新值失败的比率: " + cache.stats().loadFailureRate());
System.out.println("missCount: " + cache.stats().missCount());
System.out.println("missRate: " + cache.stats().missRate());
System.out.println("hitCount + missCount: " + cache.stats().requestCount());
System.out.println("加载新值所花费的总纳秒数: " + cache.stats().totalLoadTime());
}
}
运行结果:
加载新值所花费的平均时间 totalLoadTime / (loadSuccessCount + loadFailureCount): 23188.66
缓存逐出的数量: 0
缓存逐出的数量(权重): 0
返回命中缓存的总数: 50
返回命中与请求的比率: 0.3333333333333333
加载新值的总次数: 100
加载新值成功的总次数: 100
加载新值失败的总次数: 0
加载新值失败的比率: 0.0
missCount: 100
missRate: 0.6666666666666666
hitCount + missCount: 150
加载新值所花费的总纳秒数: 2318866
七、Caffeine的刷新
刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。
public class CaffeineRefreshTest {
@Test
public void test() throws InterruptedException {
System.out.println("开始时间: " + LocalDateTime.now());
System.out.println();
LoadingCache<Integer, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(5))
// TODO 注意这里的刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。
.refreshAfterWrite(Duration.ofSeconds(2))
.build(new CacheLoader<Integer, String>() {
/**
* 刷新数据逻辑,调用此方法的线程,获取的是旧数据
*
* @param key
* @return
* @throws Exception
*/
@Nullable
@Override
public String load(@NonNull Integer key) throws Exception {
System.out.println("刷新时间: " + LocalDateTime.now());
System.out.println("2当前所在线程:" + Thread.currentThread().getName());
int i = RandomUtils.nextInt(0, 20);
System.out.println(i);
System.out.println();
if (i % 2 == 0) {
return "wxyz";
} else {
return "WXYZ";
}
}
}
);
cache.put(1, "abcd");
String value1 = cache.get(1);
System.out.println("第1次访问key=1:" + value1);
System.out.println("1当前所在线程:" + Thread.currentThread().getName());
System.out.println();
// 触发刷新后,第一次返回旧数据
Thread.sleep(3000);
System.out.println("3s后访问key=1:" + cache.get(1));
System.out.println();
// 返回新数据
String value2 = cache.get(1);
String result2 = value2.equals(value1) ? "false" : "true";
System.out.println("第3次获取key=1:" + value2 + ",是否刷新值=" + result2);
System.out.println();
// 触发刷新后,第一次返回旧数据
Thread.sleep(3000);
String value3 = cache.get(1);
String result3 = value3.equals(value2) ? "false" : "true";
System.out.println("6s次后访问key=1:" + value3 + ",是否刷新值=" + result3);
Thread.sleep(100);
// 返回新数据
String value4 = cache.get(1);
String result4 = value4.equals(value3) ? "false" : "true";
System.out.println("第4次获取key=1:" + value4 + ",是否刷新值=" + result4);
}
}
运行结果:
开始时间: 2020-12-31T17:42:02.721
第1次访问key=1:abcd
1当前所在线程:main
刷新时间: 2020-12-31T17:42:05.784
2当前所在线程:ForkJoinPool.commonPool-worker-1
11
3s后访问key=1:abcd
第3次获取key=1:WXYZ,是否刷新值=true
6s次后访问key=1:WXYZ,是否刷新值=false
刷新时间: 2020-12-31T17:42:08.799
2当前所在线程:ForkJoinPool.commonPool-worker-1
6
第4次获取key=1:wxyz,是否刷新值=true
八、总结:
1、掌握Caffeine的3种填充策略:
- 手动填充数据
- 同步加载
- 异步加载
2、掌握Caffeine提供了3种回收策略:
- 基于大小回收
- 基于时间回收
- 基于引用回收
3、Caffeine的移除监听器
如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。这个时候RemovalListener就派上用场了。
删除侦听器的里面的操作是使用Executor来异步执行的。默认执行程序是ForkJoinPool.commonPool(),可以通过Caffeine.executor(Executor)覆盖。
4、Caffeine的CacheWriter
CacheWriter允许缓存充当一个底层资源的代理,当与CacheLoader结合使用时,所有对缓存的读写操作都可以通过Writer进行传递。Writer可以把操作缓存和操作外部资源扩展成一个同步的原子性操作。并且在缓存写入完成之前,它将会阻塞后续的更新缓存操作,但是读取(get)将直接返回原有的值。 如果写入程序失败,那么原有的key和value的映射将保持不变,如果出现异常将直接抛给调用者。
能监听到的动作:
- CacheWriter可以同步的监听到缓存的创建、变更和删除操作。
不能监听到的动作:
- 加载(例如,LoadingCache.get)、重新加载(例如,LoadingCache.refresh)和计算(例如Map.computeIfPresent)的操作不被CacheWriter监听到。
注意事项:
- CacheWriter不能与弱键或AsyncLoadingCache一起使用。
应用场景:
- 可以配合外部存储使用
5、Caffeine的统计
6、Caffeine的刷新
- 刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。