赞
踩
目录
Caffeine是一种高性能的缓存库,是基于Java 8的最佳(最优)缓存框架。
Caffeine:是对Guava缓存组件的重写版本,虽然功能不如EHCache多,但是其提供了最优的缓存命中率。
Caffeine提供一个性能卓越的本地缓存(local cache) 实现, 也是SpringBoot内置的本地缓存实现。(Caffeine性能是Guava Cache的6倍)
可以自动将数据加载到缓存之中,也可以采用异步的方式进行加载
当基于频率和最近访问的缓存达到最大容量时,该组件会自动切换到基于大小的模式
可以根据上一次缓存访问或上一次的数据写入来决定缓存的过期处理
当某一条缓存数据出现了过期访问后可以自动进行异步刷新
考虑到JVM 内存的管理机制,所有的缓存 KEY自动包含在弱引用之中,VALUE 包含在弱引用或软引用中
当缓存数据被清理后,将会收到相应的通知信息
缓存数据的写入可以传播到外部存储
自动记录缓存数据被访问的次数
Cache是一个核心的接口,里面定义了很多方法,我们要使用缓存一般是使用Cache的的子类,根据官方的方法,我们通过caffeine这个类来获得实现Cache的类。
- Cache<String,String> cache = Caffeine.newBuilder()
- .maximumSize(100)//设置缓存数量
- //设置超时时间
- .expireAfterAccess(3L, TimeUnit.SECONDS)
- .build();
-
- cache.put("baidu","www.baidu.com");
- cache.put("edu","www.edu.com");
- cache.put("book","Spring缓存实战");
-
- System.out.println("baidu = " + cache.getIfPresent("baidu"));
-
- TimeUnit.SECONDS.sleep(5);
- System.out.println("baidu = " + cache.getIfPresent("baidu"));
Caffeine 相当于一个缓存工厂,可以创建出多个缓存实例 Cache。这些缓存实例都继承了 Caffeine 的参数配置,Caffeine 是如何配置的,这些缓存实例就具有什么样的特性和功能。
通过Caffeine的builder进行构建,可以将Caffeine理解成一个工厂,通过给工厂设定“指标”获得想要的产品。
这里就是设置了最大缓存数量,还有缓存数据的过期时间,然后得到的Cache对象就具有这两个特点。下面是运行结果:
baidu = www.baidu.com
baidu = null
通过put( )方法和invalidate( )方法进行缓存数据的操作
- Cache<String, String> cache = Caffeine.newBuilder()
- .build();
- cache.put("name","张三");//添加一个缓存
- System.out.println(cache.getIfPresent("name"));
- cache.invalidate("name");//手动清楚key为name的缓存
- System.out.println(cache.getIfPresent("name"));
运行结果:
张三 null
有些时候当缓存数据失效的时候,我们可能希望拿到缓存的时候不返回一个null,可以进行一些数的处理,自定义的进行一些操作,这就要用到Cache接口的get方法,这个get方法里面可以实现一个函数式接口,让我们对数据进行自定义的处理:
- System.out.println("baidu = " + cache.getIfPresent("baidu"));
- TimeUnit.SECONDS.sleep(5);//暂停5秒,让数据失效
- String baidu = cache.get("baidu", (key) -> {
- System.out.println("正在进行缓存失效处理...");
- //假设下面的try语句块是在进行缓存的失效处理
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return "key为" + key + "的缓存已失效";
- });
- System.out.println("baidu = " + baidu);
当我们获取key为”baidu”的缓存值的时候,如果没有过期,则返回缓存值,如果过期了,则会进入该函数式接口进行处理,下面是运行结果:
baidu = www.baidu.com 正在进行缓存失效处理... baidu = key为baidu的缓存已失效
这里值得一提的是,如果数据已经过期,然后调用了get方面里面的函数式接口之后,会自动的给缓存重新赋值,赋的值就是return返回的值。
在之前进行缓存数据查询的时候,曾经使用过一个Cache 接口中提供的get0方法,这个方法可以结合Function 接口在缓存数据已经失效之后进行数据的加载使用,首先来观察一下这个方法的基本定义。
V get(K key, Function<? super K, ? extends @PolyNull V> mappingFunction);
这种数据加载操作指的是在缓存数据不存在的时候进行数据的同步加载处理操作,而除了此种加载操作的机制之外,在缓存组件之中还提供有一个较为特殊的 Cacheloader 接口,这个接口的触发机制有些不太一样,它所采用的依然是同步的加载处理。
这个机制跟上面的可以看作是一样的,都是在数据过期之后,进行数据的再次加载(就是再次放进缓存中)
处理流程是:
1)首先在builder()的时候写上一个函数式接口(编写重新加载数据的流程)
2)获取数据的时候,通过getAll( )方法触发builder中的函数式接口流程,进行重新加载数据。
- LoadingCache<String,String> cache = Caffeine.newBuilder()
- .maximumSize(100)
- .expireAfterAccess(3L, TimeUnit.SECONDS)
- .build(new CacheLoader<String, String>() {
- @Override
- public @Nullable String load(@NonNull String key) throws Exception {
- System.out.println(key+": 正在重新加载数据...");
- //这里进行数据的再次加载,例如:从数据库中再次读取失效的数据
- //return之后就会再次加载进缓存
- return "key为"+key+"重新加载过后的数据";
- }
- });
-
- cache.put("baidu","www.baidu.com");
- System.out.println("baidu = " + cache.getIfPresent("baidu"));
- //休息4秒,让数据过期
- TimeUnit.SECONDS.sleep(4);
- cache.put("baidu","www.baidu.com");
- System.out.println("baidu = " + cache.get("baidu"));
- TimeUnit.SECONDS.sleep(4);
- System.out.println(cache.get("baidu"));
- }
运行结果是:
baidu = www.baidu.com baidu: 正在重新加载数据... key为baidu重新加载过后的数据
两种同步加载的区别
与之前的 get0的同步加载操作不同的是,此时是使用了专属的功能接口完成了数据的加载,从实现的结构上来说的更加的标准化,同时也符合于 Caffeine 自己的设计要求。
第一种方式是针对于临时的一种使用方法,第二种更加的统一更加的具有模板效应
这里不过多的赘述异步和同步的区别了,我就介绍一下异步加载缓存的流程和同步的区别。假如你在拿去缓存数据的时候,如果有3个值都过期了,你使用的同步的方式得依次加载,但是你使用异步的方式,就能同时进行加载。
这里的异步的参照是针对于不同的key加载数据,在同一个key加载数据的时候,如果没有加载出来是不会执行后面的结果。
这里使用builAsync()方法,创造缓存对象,返回对象的接口是AsyncLoadingCache;
AsyncLoadingCache的父接口是AsyncCache,而AsycnCache和Cache接口是同级的。
异步加载的代码案例(提示:不管是异步还是同步都能使用cache.getAll()方法,这个方法的入参是一个装有key的列表,返回值是一个map;在异步中,返回值的map被CompletableFuture包裹住的)
- AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
- .expireAfterAccess(3L, TimeUnit.SECONDS)
- .buildAsync(new CacheLoader<String, String>() {
- @Override
- public @Nullable String load(@NonNull String key) throws Exception {
- System.out.println(key+": 正在重新加载...");
- TimeUnit.SECONDS.sleep(2);
- return "key为"+key+"重新加载过后的数据";
- }
- });
-
- long l = System.currentTimeMillis();
- //使用了异步的缓存之后,缓存的值都是被CompletableFuture给包裹起来的
- //所以在追加缓存和得到缓存的时候要通过操作CompletableFuture来进行
- cache.put("name",CompletableFuture.completedFuture("张三"));
- cache.put("age",CompletableFuture.completedFuture("18"));
-
- //获取没过期的缓存
- System.out.println(cache.get("name").get());
- System.out.println(cache.get("age").get());
- TimeUnit.SECONDS.sleep(5);
-
- //创建key的列表,通过cache.getAll()拿到所有key对应的值
- ArrayList<String> keys = new ArrayList<>();
- keys.add("name");
- keys.add("age");
- //拿到keys对应缓存的值
- Map<String, String> map = cache.getAll(keys).get();
- for (Map.Entry<String, String> entry : map.entrySet()) {
- System.out.println(entry.getValue());
- }
- System.out.println("所耗时间:"+(System.currentTimeMillis()-l));
运行结果:
张三 18 name: 正在重新加载... age: 正在重新加载... key为name重新加载过后的数据 key为age重新加载过后的数据 所耗时间:7037
可以看到,同时拿两个值的数据,他们是同时进入加载操作的,实现了异步的效果。如下给出不使用异步的运行结果: 使用同步正好慢了两秒左右!!!
张三 18 name: 正在重新加载... age: 正在重新加载... key为name重新加载过后的数据 key为age重新加载过后的数据 所耗时间:9103
缓存之中的数据内容不可能一直被保留,因为只要时间一到,缓存就应该将数据进行驱逐,但是除了时间之外还需要考虑到个问题,缓存数据满了之后呢?是不是也应该进行一些无用数据的驱逐处理呢?
Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)。
最大容量,如果缓存中的数据量超过这个数值,Caffeine 会有一个异步线程来专门负责清除缓存,按照指定的清除策略来清除掉多余的缓存。注意:比如最大容量是 2,此时已经存入了2个数据了,此时存入第3个数据,触发异步线程清除缓存,在清除操作没有完成之前,缓存中仍然有3个数据,且 3 个数据均可读,缓存的大小也是 3,只有当缓存操作完成了,缓存中才只剩 2 个数据,至于清除掉了哪个数据,这就要看清除策略了。
- Cache<String, String> cache = Caffeine.newBuilder()
- //将最大数量设置为一
- .maximumSize(1)
- .expireAfterAccess(3L, TimeUnit.SECONDS)
- .build();
- cache.put("name","张三");
- cache.put("age","18");
- TimeUnit.MILLISECONDS.sleep(100);
- System.out.println(cache.getIfPresent("name"));
- System.out.println(cache.getIfPresent("age"));
可以看到,"name"的数据已经被清除了
最大容量 和 最大权重 只能二选一作为缓存空间的限制。
最大权重,存入缓存的每个元素都要有一个权重值,当缓存中所有元素的权重值超过最大权重时,就会触发异步清除。
要使用权重来衡量的话,就要规定权重是什么,每个元素的权重怎么计算,weigher 方法就是设置权重规则的,它的参数是一个函数,函数的参数是 key 和 value,函数的返回值就是元素的权重。
- public void test5() throws InterruptedException {
- Cache<String, String> cache = Caffeine.newBuilder()
- //将最大数量设置为一
- .maximumWeight(100)
- .weigher(((key, value) -> {
- System.out.println("【权重计算器】,key="+key+" value="+value);
- //这里直接返回一个固定的权重,真实开发会有一些业务的运算
- return 50;
- }))
- .expireAfterAccess(3L, TimeUnit.SECONDS)
- .build();
- cache.put("name","张三");
- cache.put("age","18");
- cache.put("sex","男");
- TimeUnit.MILLISECONDS.sleep(100);
- System.out.println(cache.getIfPresent("name"));
- System.out.println(cache.getIfPresent("age"));
- System.out.println(cache.getIfPresent("sex"));
- }
运行结果:第一个数据被清除了,因为第三个进来权重大于100了
【权重计算器】,key=name value=张三 【权重计算器】,key=age value=18 【权重计算器】,key=sex value=男 null 18 男
我们将权重计算的地方,改为返回51,运行结果:第一个和第二个数据被清除了
【权重计算器】,key=name value=张三 【权重计算器】,key=age value=18 【权重计算器】,key=sex value=男 null null 男
时间驱逐策略其实在上面我们已经用到过了,就是通过:.expireAfterAccess( )方法控制的。
但是这里有个注意点:时间驱逐策略分为最后一次读,和写入过后。分布代表最后一次读过后【.expireAfterAccess( )】开始计时,计时结束,清除缓存;写入过后【.expireAfterWrite( )】开始计时,计时结束,清除缓存;
根据最后一次读
我们每隔一秒就读一次
- Cache<String, String> cache = Caffeine.newBuilder()
- //将最大数量设置为一
- .expireAfterAccess(3L,TimeUnit.SECONDS)
- .build();
- cache.put("name","张三");
- for (int i = 0; i < 1000; i++) {
- System.out.println("第"+i+"次读:"+cache.getIfPresent("name"));
- TimeUnit.SECONDS.sleep(1);
- }
运行结果:可以看到一直能读到数据,因为每读一次,就重新计时
第0次读:张三 第1次读:张三 第2次读:张三 ...
第999次读:张三
根据写入过后
可以在短信验证码的场景使用
还是每秒读一次
- public void test6() throws InterruptedException {
- Cache<String, String> cache = Caffeine.newBuilder()
- //将最大数量设置为一
- .expireAfterWrite(3L,TimeUnit.SECONDS)
- .build();
- cache.put("name","张三");
- for (int i = 0; i < 1000; i++) {
- System.out.println("第"+i+"次读:"+cache.getIfPresent("name"));
- TimeUnit.SECONDS.sleep(1);
- }
- }
运行结果:从写入的第三秒过后,数据就驱逐了
第0次读:张三 第1次读:张三 第2次读:张三 第3次读:null 第4次读:null
....
第999次读:null
定制化的缓存驱逐的策略可以通过 Expiry 接口来实现,这个接口内部定义有几个方法,分别代表的是:创建后(多久失效)、更行后(多久失效)、读取后(多久失效)
使用 .expireAfter( )的方式启用这个自定义的策略,里面入参自己写的策略。
首先创建一个类MyExpire,实现Expire接口,重写三个方法代表上面三种策略
- public class MyExpire implements Expiry<String,String> {
- @Override
- public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
- //创建后
- System.out.println("创建后,失效计算 -- "+key+": "+value);
- //将两秒转换为纳秒,并返回;代表创建后两秒失效
- return TimeUnit.NANOSECONDS.convert(2,TimeUnit.SECONDS);
- }
-
- @Override
- public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
- //更新后
- System.out.println("更新后,失效计算 -- "+key+": "+value);
- return TimeUnit.NANOSECONDS.convert(5,TimeUnit.SECONDS);
- }
-
- @Override
- public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
- //读取后
- System.out.println("读取后,失效计算 -- "+key+": "+value);
- return TimeUnit.NANOSECONDS.convert(3,TimeUnit.SECONDS);
- }
- }
然后将自己的策略应用上去:
- public void test7() throws InterruptedException {
- Cache<String, String> cache = Caffeine.newBuilder()
- //将最大数量设置为一
- .expireAfter(new MyExpire())
- .build();
- cache.put("name","张三");
- for (int i = 0; i < 1000; i++) {
- System.out.println("第"+i+"次读:"+cache.getIfPresent("name"));
- TimeUnit.SECONDS.sleep(1);
- }
- }
运行结果:
创建后,失效计算 -- name: 张三 读取后,失效计算 -- name: 张三 第0次读:张三 读取后,失效计算 -- name: 张三 第1次读:张三 读取后,失效计算 -- name: 张三 第2次读:张三
由于此时每次读取数据的时候,都要进行一次重新的失效计算操作,所以间隔 1 秒的读取的话,是可以持续读取到数据
Caffeine 开发组件有一个最为重要的特点是自带有数据的统计功能,例如:你的缓存查询了多少次,有多少次是查询准确(指定数据的 KEY 存在并且可以返回最终的数据),查询有多少次是失败的。默认情况下是没有开启此数据统计信息,如果要想获取到统计信息,则需要使用到 Caffeine开发类提供的处理方法。
通过在build之前,添加recordStats()
来开启数据统计功能
以上的操作仅仅是开启了数据统计的处理支持,但是如果要想最终获取到这些统计的信息,那么就需要提供有另外一个方法的支持,这个方法是由 Cache接口来定义的。使用cache.stats( )方法获得CacheStats对象,然后就能访问各种数据了。
- Cache<String, String> cache = Caffeine.newBuilder()
- .maximumSize(100)
- .recordStats()
- .expireAfterAccess(200L,TimeUnit.MILLISECONDS)
- .build();
- cache.put("name","张三");
- cache.put("age","18");
- cache.put("sex","男");
- //设置的key有些是不存在的,通过这些不存在的进行非命中操作
- String[] keys = new String[]{"name","age","sex","phone","school"};
- for (int i = 0; i < 1000; i++) {
- cache.getIfPresent(keys[new Random().nextInt(keys.length)]);
- }
- CacheStats stats = cache.stats();
- System.out.println("命中个数:"+stats.hitCount());
- System.out.println("命中率:"+stats.hitRate());
上面执行了一千次数据的访问,结果是:
命中个数:590 命中率:0.59
除了命中率,CacheStats还有很多的其他数据:
totalLoadTime: 总共加载时间。
loadFailureRate: 加载失败率,= 总共加载失败次数 / 总共加载次数
averageLoadPenalty : 平均加载时间,单位-纳秒
evictionCount: 被淘汰出缓存的数据总个数
evictionWeight: 被淘汰出缓存的那些数据的总权重
hitCount: 命中缓存的次数
hitRate: 命中缓存率
loadCount: 加载次数
loadFailureCount: 加载失败次数
loadSuccessCount: 加载成功次数
missCount: 未命中次数
missRate: 未命中率
requestCount: 用户请求查询总次数
caffeine支持自定义状态收集,每当缓存有操作发生时,都会使得缓存的某些状态指标发生改变,哪些状态指标发生了改变,就会自动触发收集器中对应的方法执行。什么意思:例如命中一次之后,我们可以写入一个日志。就相当于是一个事件监听器。
收集器接口为 StatsCounter ,我们只需实现这个接口,重写方法就能自定义行为。(我这里用命中进行测试,实际场景下完全可以做自己的自定义操作)
- public class MyStatsCounter implements StatsCounter {
- @Override
- public void recordHits(@NonNegative int count) {
- System.out.println("命中之后执行的操作");
- }
-
- @Override
- public void recordMisses(@NonNegative int count) {
-
- }
-
- @Override
- public void recordLoadSuccess(@NonNegative long loadTime) {
-
- }
-
- @Override
- public void recordLoadFailure(@NonNegative long loadTime) {
-
- }
-
- @Override
- public void recordEviction() {
-
- }
-
- @Override
- public @NonNull CacheStats snapshot() {
- return null;
- }
- }
写好接口之后,在build的时候跟某个缓存挂钩
- Cache<String, String> cache = Caffeine.newBuilder()
- .maximumSize(100)
- .recordStats(()->new MyStatsCounter())
- .expireAfterAccess(200L,TimeUnit.MILLISECONDS)
- .build();
- cache.put("name","张三");
-
- System.out.println(cache.getIfPresent("name"));
运行结果:正常运行
命中之后执行的操作 张三
同步监听器是在创建缓存数据和移除缓存数据的时候调用的,可以做一些自定义操作,是基于接口CacheWriter实现的,首先我们先实现这个接口
- public class MyCacheWriter implements CacheWriter<String,String> {
- @Override
- public void write(@NonNull String key, @NonNull String value) {
- System.out.println("这里是同步监听写操作"+key+" "+value);
- }
-
- @Override
- public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause cause) {
- System.out.println("这里是同步监听删操作"+key+" "+value);
- }
- }
然后在build获得缓存时期跟cache挂钩
- Cache<String, String> cache = Caffeine.newBuilder()
- .maximumSize(100)
- .writer(new MyCacheWriter())
- .expireAfterAccess(200L,TimeUnit.MILLISECONDS)
- .build();
- cache.put("name","张三");
- System.out.println(cache.getIfPresent("name"));
- cache.invalidate("name");
- TimeUnit.SECONDS.sleep(2);
运行结果:
这里是同步监听写操作name 张三 张三 这里是同步监听删操作name 张三
缓存中的数据发送更新,或者被清除时,就会触发监听器,在监听器里可以自定义一些处理手段。可以查看哪个数据被清除,清除的原因等。这个触发和监听的过程是异步的。
- Cache<String, String> cache = Caffeine.newBuilder()
- .maximumSize(100)
- .removalListener(((key, value, cause) -> System.out.println("键:"+key+" 值:"+value+" 清除原因:"+cause)))
- .expireAfterAccess(200L,TimeUnit.MILLISECONDS)
- .build();
- cache.put("name","张三");
- System.out.println(cache.getIfPresent("name"));
- cache.invalidate("name");
- TimeUnit.SECONDS.sleep(2);
运行结果:
张三 键:name 值:张三 清除原因:EXPLICIT
AsyncCache 缓存不支持软引用和弱引用。
weakKeys():将缓存的 key 使用弱引用包装起来,只要 GC 的时候,就能被回收。 weakValues():将缓存的 value 使用弱引用包装起来,只要 GC 的时候,就能被回收。 softValues():将缓存的 value使用软引用包装起来,只要 GC 的时候,有必要,就能被回收。
因此,弱引用 ,软引用的设置,只是为了方便回收空间,节省空间,但是使用的时候注意一点,缓存查询时,是用 == 来判断两个 key 是否相等,比较的是地址,不是 key 本身的内容,很容易造成一种现象:命名 key 是对的,但就是无法命中,因为 key 的内容相等,但是地址却不同,会被认为是两个 key。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。