Caffeine本地缓存详解(⼀篇就明⽩)
结论:Caffeine 是⽬前性能最好的本地缓存,因此,在考虑使⽤本地缓存时,直接选择 Caffeine 即可。
先看⼀个⼩例⼦,明⽩如何创建⼀个 Caffeine 缓存实例。
Caffeine caffeine = wBuilder()
.initialCapacity(3)
.maximumSize(4);
Cache cache = caffeine.build();
cache.put("aa",13);
System.out.IfPresent("aa"));
Caffeine 相当于⼀个缓存⼯⼚,可以创建出多个缓存实例 Cache。这些缓存实例都继承了 Caffeine 的参数配置,Caffeine 是如何配置的,这些缓存实例就具有什么样的特性和功能。
1. Caffeine 可以设置哪些缓存属性呢?
1. 缓存初始容量
initialCapacity :整数,表⽰能存储多少个缓存对象。
为什么要设置初始容量呢?因为如果提前能预估缓存的使⽤⼤⼩,那么可以设置缓存的初始容量,以免缓存不断地进⾏扩容,致使效率不⾼。
2. 最⼤容量最⼤权重
maximumSize :最⼤容量,如果缓存中的数据量超过这个数值,Caffeine 会有⼀个异步线程来专门负责清除缓存,按照指定的清除策略来清除掉多余的缓存。注意:⽐如最⼤容量是 2,此时已经存⼊了2个数据了,此时存⼊第3个数据,触发异步线程清除缓存,在清除操作没有完成之前,缓存中仍然有3个数据,且 3 个数据均可读,缓存的⼤⼩也是 3,只有当缓存操作完成了,缓存中才只剩 2 个数据,⾄于清除掉了哪个数据,这就要看清除策略了。
maximumWeight:最⼤权重,存⼊缓存的每个元素都要有⼀个权重值,当缓存中所有元素的权重值超过最⼤权重时,就会触发异步清除。下⾯给个例⼦。
class Person{
Integer age;
String name;
}
Caffeine<String, Person> caffeine = wBuilder()
.maximumWeight(30)
.weigher((String key, Person value)-> Age());
Cache<String, Person> cache = caffeine.build();
cache.put("one",new Person(12,"one"));
cache.put("two",new Person(18,"two"));
cache.put("three",new Person(1,"three"));
Thread.sleep(10);
System.out.println(cache.estimatedSize());
System.out.IfPresent("two"));
运⾏结果:
2
null
要使⽤权重来衡量的话,就要规定权重是什么,每个元素的权重怎么计算,weigher ⽅法就是设置权重规则的,它的参数是⼀个函数,函数的参数是 key 和 value,函数的返回值就是元素的权重,⽐如上述代码中,caffeine 设置了最⼤权重值为 30,然后将每个 Person 对象的 age 年龄作为权重值,所以整个意思就是:缓存中存储的是 Person 对象,但是限制所有对象的 age 总和不能超过 30,否则就触发异步清除缓存。
特别要注意⼀点:最⼤容量 和 最⼤权重 只能⼆选⼀作为缓存空间的限制。
3. 缓存状态
3.1 默认的缓存状态收集器 CacheStats
默认情况下,缓存的状态会⽤⼀个 CacheStats 对象记录下来,通过访问 CacheStats 对象就可以知道当前缓存的各种状态指标,那究竟有哪些指标呢?
先说⼀下什么是“加载”,当查询缓存时,缓存未命中,那就需要去第三⽅数据库中查询,然后将查询出的数据先存⼊缓存,再返回给查询者,这个过程就是加载。
totalLoadTime:总共加载时间。
loadFailureRate:加载失败率,= 总共加载失败次数 / 总共加载次数
averageLoadPenalty :平均加载时间,单位-纳秒
evictionCount:被淘汰出缓存的数据总个数
evictionWeight:被淘汰出缓存的那些数据的总权重
hitCount:命中缓存的次数
hitRate:命中缓存率
loadCount:加载次数
loadFailureCount:加载失败次数
loadSuccessCount:加载成功次数
missCount:未命中次数
missRate:未命中率
requestCount:⽤户请求查询总次数
CacheStats 类包含了 2 个⽅法,了解⼀下:
CacheStats minus(@Nonnull CacheStats other):当前 CacheStats 对象的各项指标减去参数 other 的各项指标,差值形成⼀个新的CacheStats 对象。
CacheStats plus(@Nonnull CacheStats other):当前 CacheStats 对象的各项指标加上参数 other 的各项指标,和值形成⼀个新的CacheStats 对象。
举个例⼦说明:
Caffeine<String, Person> caffeine = wBuilder()
.maximumWeight(30)
.recordStats()
.weigher((String key, Person value)-> Age());
Cache<String, Person> cache = caffeine.build();
cache.put("one",new Person(12,"one"));
cache.put("two",new Person(18,"two"));
cache.put("three",new Person(1,"three"));
CacheStats stats = cache.stats();
System.out.println(stats.hitCount());
3.2 ⾃定义的缓存状态收集器
⾃定义的缓存状态收集器的作⽤:每当缓存有操作发⽣时,不管是查询,加载,存⼊,都会使得缓存
的某些状态指标发⽣改变,哪些状态指标发⽣了改变,就会⾃动触发收集器中对应的⽅法执⾏,如果我们在⽅法中⾃定义的代码是收集代码,⽐如将指标数值发送到 kafka,那么其它程序从kafka读取到数值,再进⾏分析与可视化展⽰,就能实现对缓存的实时监控了。
收集器接⼝为 StatsCounter ,我们只需实现这个接⼝的所有抽象⽅法即可。下⾯举例说明。
public class MyStatsCounter implements StatsCounter {
@Override
public void recordHits(int i){
System.out.println("命中次数:"+ i);
}
@Override
public void recordMisses(int i){
System.out.println("未命中次数:"+ i);
}
@Override
public void recordLoadSuccess(long l){
System.out.println("加载成功次数:"+ l);
}
@Override
public void recordLoadFailure(long l){
System.out.println("加载失败次数:"+ l);
}
@Override
public void recordEviction(){
System.out.println("因为缓存⼤⼩限制,执⾏了⼀次缓存清除⼯作");
}
@Override
public void recordEviction(int weight){
System.out.println("因为缓存权重限制,执⾏了⼀次缓存清除⼯作,清除的数据的权重为:"+ weight);
}
@Override
public CacheStats snapshot(){
return null;
}
}
上述代码为⾃定义的缓存状态收集器,收集到的状态指标只是简单地打印出来,snapshot ⽅法有什么作⽤,暂时不清楚。
特别需要注意的是:收集器中那些⽅法得到的状态值,只是当前缓存操作所产⽣的结果,⽐如当前 IfPresent() 查询⼀个值,查询到了,说明命中了,但是 recordHits(int i) ⽅法的参数 i = 1,因为本次操作命中了 1 次。
再将收集器与某个缓存挂钩,如下:
MyStatsCounter myStatsCounter =new MyStatsCounter();
Caffeine<String, Person> caffeine = wBuilder()
.maximumWeight(30)
.recordStats(()->myStatsCounter)
.weigher((String key, Person value)-> Age());
Cache<String, Person> cache = caffeine.build();
cache.put("one",new Person(12,"one"));
cache.put("two",new Person(18,"two"));
cache.put("three",new Person(1,"three"));
CacheStats stats = myStatsCounter.snapshot();
Thread.sleep(1000);
最后的执⾏结果为:
未命中次数:1
因为缓存权重限制,执⾏了⼀次缓存清除⼯作,清除的数据的权重为:18
4. 线程池
Caffeine 缓冲池总有⼀些异步任务要执⾏,所以它包含了⼀个线程池,⽤于执⾏这些异步任务,默认使⽤的是
ForkJoinPoolmonPool() 线程池,个⼈觉得没有必要去⾃定义线程池,或者使⽤其它的线程池,因为 Caffeine 的作者在设计的时候就考虑了线程池的选择,既然别⼈选择了,就有⼀定道理。
如果⼀定要⽤其它的线程池,可以通过 executor() ⽅法设置,⽅法参数是⼀个 线程池对象。
5. 数据过期策略
5.1 expireAfterAccess
最后⼀次访问之后,隔多久没有被再次访问的话,就过期。访问包括了 读 和 写。举个例⼦:
Caffeine<String, Person> caffeine = wBuilder()
.maximumWeight(30)
.expireAfterAccess(2, TimeUnit.SECONDS)
access被淘汰了吗
.weigher((String key, Person value)-> Age());
Cache<String, Person> cache = caffeine.build();
cache.put("one",new Person(12,"one"));
cache.put("two",new Person(18,"two"));
Thread.sleep(3000);
System.out.IfPresent("one"));
System.out.IfPresent("two"));
运⾏结果:
null
null
expireAfterAccess 包含两个参数,第⼆个参数是时间单位,第⼀个参数是时间⼤⼩,⽐如上述代码中设置过期时间为 2 秒,在过了 3 秒之后,再次访问数据,发现数据不存在了,即触发过期清除了。
5.2 expireAfterWrite
某个数据在多久没有被更新后,就过期。举个例⼦
Caffeine<String, Person> caffeine = wBuilder()
.maximumWeight(30)
.expireAfterWrite(2, TimeUnit.SECONDS)
.weigher((String key, Person value)-> Age());
Cache<String, Person> cache = caffeine.build();
cache.put("one",new Person(12,"one"));
cache.put("two",new Person(18,"two"));
Thread.sleep(1000);
System.out.IfPresent("one").getName());
Thread.sleep(2000);
System.out.IfPresent("one"));
运⾏结果:
one
null
只能是被更新,才能延续数据的⽣命,即便是数据被读取了,也不⾏,时间⼀到,也会过期。
5.2 expireAfter
实话实说,关于这个设置项,官⽹没有说明⽩,⽹上其它博客更是千篇⼀律,没有⼀个讲明⽩的。此处简单讲讲我个⼈的测试⽤例与理解,如果有误,欢迎评论指正。
Caffeine<String, Person> caffeine = wBuilder()
.maximumWeight(30)
.expireAfter(new Expiry<String, Person>(){
@Override
public long expireAfterCreate(String s, Person person,long l){
Age()>60){//⾸次存⼊缓存后,年龄⼤于 60 的,过期时间为 4 秒
return4000000000L;
}
return2000000000L;// 否则为 2 秒
}
@Override
public long expireAfterUpdate(String s, Person person,long l,long l1){
Name().equals("one")){// 更新 one 这个⼈之后,过期时间为 8 秒
return8000000000L;
}
return4000000000L;// 更新其它⼈后,过期时间为 4 秒
}
@Override
public long expireAfterRead(String s, Person person,long l,long l1){
return3000000000L;// 每次被读取后,过期时间为 3 秒
}
})
.weigher((String key, Person value)-> Age());
Cache<String, Person> cache = caffeine.build();
expireAfter ⽅法的参数是⼀个 Expiry 对象,Expiry 是⼀个接⼝,上述代码⽤了匿名类。需要实现 Expiry 的三个⽅法。
expireAfterCreate(String s, Person person, long l) :此⽅法为数据<s , person> 创建之后,过期时间是多久(可以理解为⽣命周期),单位为纳秒,⽅法的返回值就是过期时间,这个时间设置为多久,怎么设置,可以⾃定义的,⽐如上述代码,60 岁以上的过期时间为 4 秒,如果 4 秒内数据没有被操作,就过期。另外还有⼀个参数 long l,l 表⽰创建时间的系统时间戳,单位为纳秒。
expireAfterUpdate(String s, Person person, long l, long l1):此⽅法表⽰更新某个数据后,过期时间是多久(刷新⽣命周期),个⼈认为:参数 l 表⽰更新前的系统时间戳,l1 表⽰更新成功后的系统时间戳,因为在多线程下,更新操作可能会阻塞。
expireAfterRead(String s, Person person, long l, long l1) : 与 expireAfterUpdate 同理。
6. refreshAfterWrite 延迟刷新
refreshAfterWrite(long duration, TimeUnit unit)
写操作完成后多久才将数据刷新进缓存中,两个参数只是⽤于设置时间长短的。
只适⽤于 LoadingCache 和 AsyncLoadingCache,如果刷新操作没有完成,读取的数据只是旧数据。
同理,不想写了。
7. removalListener 清除、更新监听
当缓存中的数据发送更新,或者被清除时,就会触发,在⾥可以⾃定义⼀些处理⼿段,⽐如打印出哪个数据被清除,原因是什么。这个触发和监听的过程是异步的,就是说可能数据都被删除⼀⼩会⼉了,才监听到。 举个例⼦:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。