Caffeine Cache基础入门

Caffeine Cache基础入门

Caffeine Cache 以其高性能和可扩展性赢得「 本地缓存之王 」的称号,它是一个 Java 缓存库。

Spring Boot 1.x 版本中的默认本地缓存是 Guava Cache。但在 Spring5 (SpringBoot 2.x)后,Spring 官方放弃了 Guava Cache 作为缓存机制,而是使用性能更优秀的 Caffeine 作为默认缓存组件。

Caffeine 官方测试报告:https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN

缓存介绍

image.png

Caffeine特点

1、自动将数据加载到缓存中,同时也可以采用异步的方式加载。

2、内存淘汰策略:基于频次、基于最近访问、最大容量。

3、根据上一次的缓存访问\上一次的数据写入决定缓存的过期的设置。

4、当一条缓存数据过期了,自动清理,清理的时候也是异步线程来做。

5、考虑JVM的内存管理机制,加入弱引用、软引用。

6、缓存数据被清理后,会收到相关的通知信息

7、缓存数据的写入可以传播到外部的存储。

8、统计功能:被访问次数,命中,清理的个数,加载个数

Caffeine Cache入门

官网地址:https://github.com/ben-manes/caffeine

Maven引入

   <!-- Spring boot Cache-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!--for caffeine cache-->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.7.0</version>
        </dependency>

Cache是一个核心的接口,里面定义了很多方法,我们要使用缓存一般是使用Cache的的子类,根据官方的方法,我们通过caffeine这个类来获得实现Cache的类。

基本使用-简单

Cache是一个核心的接口,里面定义了很多方法,我们要使用缓存一般是使用Cache的的子类,根据官方的方法,我们通过caffeine这个类来获得实现Cache的类。

 public static void Cache() throws Exception{
        Cache<String, String> cache = Caffeine.newBuilder()//构建一个新的Caffeine实例
                .maximumSize(100)//设置缓存中保存的最大数量
                .expireAfterAccess(3L, TimeUnit.SECONDS)//如无访问则3秒后失效
                .build();//构建Cache接口实例

        cache.put("mca","www.mashibing.com");//设置缓存项
        cache.put("baidu","www.baidu.com");//设置缓存项
        cache.put("spring","www.spring.io");//设置缓存项

        log.info("获取缓存[getIfPresent]:mca={}",cache.getIfPresent("mca"));//获取数据
        TimeUnit.SECONDS.sleep(5);//休眠5秒

        log.info("获取缓存[getIfPresent]:mca={}",cache.getIfPresent("mca"));//获取数据

    }

最普通的一种缓存,无需指定加载方式,需要手动调用 put()进行加载。需要注意的是,put()方法对于已存在的 key 将进行覆盖。如果这个值不存在,调用 getIfPresent()方法,则会立即返回 null,不会被阻塞。

上面显示效果如下:

image.png

Caffeine 配置说明:

  • initialCapacity=[integer]: 初始的缓存空间大小
  • maximumSize=[long]: 缓存的最大条数,如果达到最大个数,默认会丢弃最早添加的
  • maximumWeight=[long]: 缓存的最大权重
  • expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
  • expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
  • refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
  • weakKeys: 打开 key 的弱引用
  • weakValues:打开 value 的弱引用
  • softValues:打开 value 的软引用
  • recordStats:开发统计功能

基本使用-过期数据的同步加载1

有些时候当缓存数据失效的时候,我们可能希望拿到缓存的时候不返回一个null,可以进行一些数的处理,自定义的进行一些操作,这就要用到Cache接口的get方法,这个get方法里面可以实现一个函数式接口,让我们对数据进行自定义的处理:

 public static void CacheExpire() throws Exception{
        Cache<String, String> cache = Caffeine.newBuilder()//构建一个新的Caffeine实例
                .maximumSize(100)//设置缓存中保存的最大数量
                .expireAfterAccess(3L, TimeUnit.SECONDS)//如无访问则3秒后失效
                .build();//构建Cache接口实例

        cache.put("mca","www.mashibing.com");//设置缓存项
        cache.put("baidu","www.baidu.com");//设置缓存项
        cache.put("spring","www.spring.io");//设置缓存项

        TimeUnit.SECONDS.sleep(5);//休眠5秒
        log.info("获取缓存[getIfPresent]:baidu={}",cache.getIfPresent("baidu"));//获取数据

        log.info("获取缓存[get]获取缓存:baidu={}",cache.get("baidu",(key)->{
            log.info("进入[失效处理]函数");
            try {
                TimeUnit.SECONDS.sleep(3);//休眠3秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.info("[失效处理]:mca={}",cache.getIfPresent("mca"));//失效处理
            return key.toUpperCase();
        }));
    }

显示效果:

image.png

如果数据已经过期,然后调用了get方面里面的函数式接口之后,会自动的给缓存重新赋值,赋的值就是return返回的值。同时看打印的时间,就知道这里的重新赋值是同步的,会阻塞的。

基本使用-过期数据的同步加载2

Caffeine还提供有一个较为特殊的 Cacheloader 接口,这个接口的触发机制有些不太一样,它所采用的依然是同步的加载处理。

处理流程是:

1)首先在builder()的时候写上一个函数式接口(编写重新加载数据的流程)

2)获取数据的时候,通过getAll( )方法触发builder中的函数式接口流程,进行重新加载数据。

public static void LoadingCache() throws Exception{
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)//设置缓存中保存的最大数量
                .expireAfterAccess(3L, TimeUnit.SECONDS)//如无访问则3秒后失效
                .build(new CacheLoader<String, String>() {
                    @Override
                    public  String load( String key) throws Exception {
                        log.info("正在重新加载数据...");
                        TimeUnit.SECONDS.sleep(1);
                        return key.toUpperCase();
                    }

                });

        cache.put("mca","www.mashibing.com");//设置缓存项
        cache.put("baidu","www.baidu.com");//设置缓存项
        cache.put("spring","www.spring.io");//设置缓存项

        TimeUnit.SECONDS.sleep(5);

        //创建key的列表,通过cache.getAll()拿到所有key对应的值
        ArrayList<String> keys = new ArrayList<>();
        keys.add("mca");
        keys.add("baidu");
        keys.add("spring");
        //拿到keys对应缓存的值
        Map<String, String> map = cache.getAll(keys);
        for (Map.Entry<String, String> entry : map.entrySet()) {
            log.info("缓存的键:{}、缓存值:{}",entry.getKey(),entry.getValue());//获取数据
        }
        log.info("LoadingCache 方法结束");

    }

运行效果:

image.png

与之前的 get()的同步加载操作不同的是,这里使用了专属的功能接口完成了数据的加载,从实现的结构上来说的更加的标准化,符合于 Caffeine 自己的设计要求。

第一种方式是针对于临时的一种使用方法,第二种更加的统一,同时有模板效应

基本使用-过期数据的异步加载

假如你在拿去缓存数据的时候,如果有3个值都过期了,你使用的同步的方式得依次加载,这样阻塞等待的时间较长,所以这里可以使用异步的方式,就能同时进行加载。

 public static void AsyncLoadingCache () throws Exception{
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)//设置缓存中保存的最大数量
                .expireAfterAccess(3L, TimeUnit.SECONDS)
                .buildAsync(new CacheLoader<String, String>() {
                    @Override
                    public  String load( String key) throws Exception {
                        log.info("正在重新加载数据...");
                        TimeUnit.SECONDS.sleep(1);
                        return key.toUpperCase();
                    }
                });
        //使用了异步的缓存之后,缓存的值都是被CompletableFuture给包裹起来的
        //所以在追加缓存和得到缓存的时候要通过操作CompletableFuture来进行
        cache.put("mca",CompletableFuture.completedFuture("www.mashibing.com"));//设置缓存项
        cache.put("baidu",CompletableFuture.completedFuture("www.baidu.com"));//设置缓存项
        cache.put("spring",CompletableFuture.completedFuture("www.spring.io"));//设置缓存项

        TimeUnit.SECONDS.sleep(5);

        //创建key的列表,通过cache.getAll()拿到所有key对应的值
        ArrayList<String> keys = new ArrayList<>();
        keys.add("mca");
        keys.add("baidu");
        keys.add("spring");
        //拿到keys对应缓存的值
        Map<String, String> map = cache.getAll(keys).get();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            log.info("缓存的键:{}、缓存值:{}",entry.getKey(),entry.getValue());//获取数据
        }
        log.info("AsyncLoadingCache 方法结束");
    }

显示效果:

image.png

AsyncLoadingCache的父接口是AsyncCache,而AsycnCache和Cache接口是同级的。

image.png

缓存淘汰机制

缓存之中的数据内容不可能一直被保留,因为只要时间一到,缓存就应该将数据进行驱逐,但是除了时间之外还需要考虑到个问题,缓存数据满了之后呢?是不是也应该进行一些无用数据的驱逐处理呢?

Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)

基于大小

最大容量 和 最大权重 只能二选一作为缓存空间的限制

最大容量

最大容量,如果缓存中的数据量超过这个数值,Caffeine 会有一个异步线程来专门负责清除缓存,按照指定的清除策略来清除掉多余的缓存。

public static void ExpireMaxType() throws Exception{
        //Caffeine 会有一个异步线程来专门负责清除缓存
        Cache<String, String> cache = Caffeine.newBuilder()
                //将最大数量设置为一
                .maximumSize(1)
                .expireAfterAccess(3L, TimeUnit.SECONDS)
                .build();
        cache.put("name","张三");
        cache.put("age","18");
        System.out.println(cache.getIfPresent("name"));
        TimeUnit.MILLISECONDS.sleep(100);
        System.out.println(cache.getIfPresent("name"));
        System.out.println(cache.getIfPresent("age"));

    }

image.png

可以看到,"name"的数据已经被清除了

最大权重

最大权重,存入缓存的每个元素都要有一个权重值,当缓存中所有元素的权重值超过最大权重时,就会触发异步清除。

weigher 方法设置权重规则。

 public static void ExpireWeigherType() throws Exception{
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumWeight(100)
                .weigher(((key, value) -> {
                    System.out.println("权重处理,key="+key+" value="+value);
                    //这里直接返回一个固定的权重,真实开发会有一些业务的运算
                    if(key.equals("age")){
                        return 30;
                    }
                    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"));
    }

image.png

运行结果:第一个数据被清除了,因为第三个进来权重大于100,导致被清理。

最后一次读

public static void ExpireAfterAccess() throws Exception{
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterAccess(1L,TimeUnit.SECONDS)
                .build();
        cache.put("name","张三");
        for (int i = 0; i < 10; i++) {
            System.out.println("第"+i+"次读:"+cache.getIfPresent("name"));
            TimeUnit.SECONDS.sleep(2);
        }
    }

最后一次写

 //时间驱逐策略--最后一次写
    public static void ExpireAfterWrite() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(1L,TimeUnit.SECONDS)
                .build();
        cache.put("name","张三");
        for (int i = 0; i < 10; i++) {
            System.out.println("第"+i+"次读:"+cache.getIfPresent("name"));
            TimeUnit.SECONDS.sleep(1);
        }
    }

自定义失效策略

public static void MyExpire() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfter(new MyExpire())
                .build();
        cache.put("name", "张三");
        for (int i = 0; i < 10; i++) {
            System.out.println("第" + i + "次读:" + cache.getIfPresent("name"));
            TimeUnit.SECONDS.sleep(1);
        }
    }
package cn.db.caffeine;

import com.github.benmanes.caffeine.cache.Expiry;

import java.util.concurrent.TimeUnit;

class MyExpire implements Expiry<String,String> {
    //创建后(多久失效)
    @Override
    public long expireAfterCreate( String key,  String value, long currentTime) {
        //创建后
        System.out.println("创建后,失效计算 -- "+key+": "+value);
        //将两秒转换为纳秒,并返回;代表创建后两秒失效
        return TimeUnit.NANOSECONDS.convert(2,TimeUnit.SECONDS);
    }
    //更行后(多久失效)
    @Override
    public long expireAfterUpdate( String key,  String value, long currentTime,  long currentDuration) {
        //更新后
        System.out.println("更新后,失效计算 -- "+key+": "+value);
        return TimeUnit.NANOSECONDS.convert(5,TimeUnit.SECONDS);
    }
    //读取后(多久失效)
    @Override
    public long expireAfterRead( String key,  String value, long currentTime,  long currentDuration) {
        //读取后
        System.out.println("读取后,失效计算 -- "+key+": "+value);
        return TimeUnit.NANOSECONDS.convert(100,TimeUnit.SECONDS);
    }
}


image.png

基于引用驱逐策略

软引用
 //基于引用驱逐策略--软引用:-Xms20m -Xmx20m
    public static void ExpireSoft() throws InterruptedException {
        Cache<String, Object> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .softValues()
                .build();

        cache.put("name",new SoftReference<>("张三"));
        System.out.println("第1次读:"+cache.getIfPresent("name"));
        List<byte[]> list = new LinkedList<>();
        try {
            for(int i=0;i<100;i++) {
                list.add(new byte[1024*1024*1]); //1M的对象
            }
        } catch (Throwable e) {
            //抛出了OOM异常时
            TimeUnit.SECONDS.sleep(1);
            System.out.println("OOM时读:"+cache.getIfPresent("name"));
            System.out.println("Exception*************"+e.toString());
        }
    }

image.png

弱引用
//基于引用驱逐策略--弱引用
    public static void ExpireWeak() throws InterruptedException {
        Cache<String, Object> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .weakValues()
                .build();
        cache.put("name",new WeakReference<>("张三"));

        System.out.println("第1次读:"+cache.getIfPresent("name"));
        System.gc();//进行一次GC垃圾回收
        System.out.println("GC后读:"+cache.getIfPresent("name"));
    }

image.png

状态收集器

Caffeine 开发组件有一个最为重要的特点是自带有数据的统计功能,例如:你的缓存查询了多少次,有多少次是查询准确(指定数据的 KEY 存在并且可以返回最终的数据),查询有多少次是失败的。默认情况下是没有开启此数据统计信息,如果要想获取到统计信息,则通过在build之前,添加 recordStats()来开启数据统计功能

public static void CacheStats () throws Exception{
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(2)
                .recordStats() //开启统计功能
                .expireAfterAccess(200L,TimeUnit.SECONDS)
                .build();
        cache.put("name","张三");
        cache.put("sex","男");
        cache.put("age","18");
        //设置的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.requestCount());
        System.out.println("命中个数:"+stats.hitCount());
        System.out.println("命中率:"+stats.hitRate());
        System.out.println("未命中次数:"+stats.missCount());
        System.out.println("未命中率:"+stats.missRate());

        System.out.println("加载次数:"+stats.loadCount());
        System.out.println("总共加载时间:"+stats.totalLoadTime());
        System.out.println("平均加载时间(单位-纳秒):"+stats.averageLoadPenalty ());
        System.out.println("加载失败率:"+stats.loadFailureRate()); //加载失败率,= 总共加载失败次数 / 总共加载次数
        System.out.println("加载失败次数:"+stats.loadFailureCount());
        System.out.println("加载成功次数:"+stats.loadSuccessCount());

        System.out.println("被淘汰出缓存的数据总个数:"+stats.evictionCount());
        System.out.println("被淘汰出缓存的那些数据的总权重:"+stats.evictionWeight());


    }

caffeine支持自定义状态收集

package cn.db.caffeine;

import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;

public class MyStatsCounter implements StatsCounter {
    @Override
    public void recordHits( int count) {
        System.out.println("命中之后执行的操作");
    }
    @Override
    public void recordMisses( int count) {
    }
    @Override
    public void recordLoadSuccess( long loadTime) {
    }
    @Override
    public void recordLoadFailure( long loadTime) {
    }
    @Override
    public void recordEviction() {
    }
    @Override
    public CacheStats snapshot() {
        return null;
    }
}


清除、更新异步监听

缓存中的数据发送更新,或者被清除时,就会触发监听器,在监听器里可以自定义一些处理手段。可以查看哪个数据被清除,清除的原因等。这个触发和监听的过程是异步的。

 Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(2)
                .removalListener(((key, value, cause) -> System.out.println("键:"+key+" 值:"+value+" 清除原因:"+cause)))
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .build();
        cache.put("name","张三");
        cache.put("sex","男");
        cache.put("age","18");
        TimeUnit.SECONDS.sleep(2);
        cache.put("name2","张三");
        cache.put("age2","18");
        cache.invalidate("age2");
        TimeUnit.SECONDS.sleep(10);

image.png

Caffeine Cache是一个用于在项目开发中实现本地缓存的工具。它是基于Guava Cache设计思想的,因此如果之前使用过Guava Cache,使用Caffeine会更加容易上手。通过Caffeine类来配置Cache,可以设置缓存的参数,包括最大缓存大小、写入后过期时间等。例如,可以使用以下代码来构造一个缓存Cache实例: Cache cache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(6, TimeUnit.MINUTES).softValues().build(); 这段代码中,使用Caffeine的newBuilder()方法创建一个新的Caffeine实例,并通过链式调用方法设置了缓存的最大大小为1000,写入后的过期时间为6分钟,并开启了软引用以支持内存敏感的缓存。最后使用build()方法构建出一个Cache实例。使用这个Cache实例,我们可以进行缓存的读写操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [浅入浅出Caffeine cache](https://blog.csdn.net/weixin_40413961/article/details/119064614)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [缓存之王Caffeine Cache,性能比Guava更强,命中率更高!](https://blog.csdn.net/rlnLo2pNEfx9c/article/details/114984936)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值