一、学习目标
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 { @Testpublic void test() { // 初始化缓存,设置了1分钟的写过期,100的缓存最大个数Cache<Integer, String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();int key1 = 1;// getIfPresent(Object key):根据key从缓存中获取值,如果没有返回NULLSystem.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>() {@Overridepublic 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);
} @Testpublic 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
*/@Overridepublic String load(@NonNull Integer key) { return getFromDataBase(key);
} /**
* 在loadAll被重写的情况下,getAll将会对缓存中没有值的key分别调用CacheLoader.loadAll方法来构建缓存的值。
* @param keys
* @return
*/@Overridepublic @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));
} @Testpublic 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、基于大小回收:
基于最大缓存数量:
@Testpublic 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
{1=a, 2=b}
{1=a, 2=b, 3=c}
{2=b, 4=d}复制代码
基于权重:
/**
* 基于权重
*/@Testpublic void testMaximumWeight() {
Cache<Integer, Integer> cache = Caffeine.newBuilder()// TODO 指定指定缓存最大权重值,配合weigher使用,随着缓存大小逐渐接近最大值,会回收最近或被很少使用的缓存,maximumWeight与maximumSize不能同时使用.maximumWeight(11)// 指定权重计算方式,缓存创建或更新时触发.weigher(new Weigher<Object, Object>() {@Overridepublic @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
97:97
98:98
99:99
元素总和: 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
*/@Testpublic 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));
} /**
* 自定义到期策略
*/@Testpublic void testCustomerExpirePolicy() {
LoadingCache<Integer, Object> cache2 = Caffeine.newBuilder()
.expireAfter(new Expiry<Object, Object>() { @Overridepublic long expireAfterCreate(@NonNull Object key, @NonNull Object value, long currentTime) { return 0;
} @Overridepublic long expireAfterUpdate(@NonNull Object key, @NonNull Object value, long currentTime, @NonNegative long currentDuration) { return 0;
} @Overridepublic 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);
} @Testpublic 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 { @Testpublic void test() throws InterruptedException {
Cache<Integer, String> cache = Caffeine.newBuilder() // TODO 指定最大缓存数量,如果超过这个值,则会剔除很久没有被访问过或者不经常使用的缓存
.maximumSize(2)
.removalListener(new RemovalListener<Object, Object>() { @Overridepublic 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
{1=a, 2=b}
{1=a, 2=b, 3=c}
【日志打印】触发了 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<>(); @Testpublic void test() throws InterruptedException {
LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
.maximumSize(1)
.writer( new CacheWriter<Integer, Integer>() { /**
* 写入到外部存储
* @param key
* @param value
*/@Overridepublic 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
*/@Overridepublic 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>() { @Overridepublic 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
删除外部存储, SIZE 策略, 将key = 2
二级缓存: {}
【日志打印】触发了 SIZE 策略, 清除的key = 2, value=2
null复制代码
六、Caffeine的统计
public class CaffeineStatsTest { @Testpublic 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>() { @Overridepublic 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 { @Testpublic 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
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
第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的刷新
-
刷新并不是到期就刷新,而是对这个数据再次访问之后,才会刷新。只阻塞加载数据的线程,其余线程返回旧数据。
Memcached 是一套开源的高性能分布式内存对象缓存系统,它将所有的数据都存储在内存中,因为在内存中会统一维护一张巨大的 Hash 表,所以支持任意存储类型的数据。很多网站通过使用 Memcached 提高网站的访问速度,尤其是对于大型的需要频繁访问数据的网站。Memcached 是典型的 C/S 架构,因此需要安装 Memcached 服务端与 Memcached API 客户端。Memcached 服务端是用 C 语音编写的,而 Memcached API 客户端可以用任何语言来编写,如PHP、Python、Perl 等,并通过 Memcached 协议与 Memcached 服务端进行通信。
需要引用MongoDB.Driver.dll、MongoDB.Driver.core.dll、MongoDB.Bson.dll三个dll。1、数据库连接:public class MongoDb
//方式一
//private static readonly string connStr = "mongodb://myAdmin:123456@local