缓存性能之王caffeine使用文档

本文详细解读Caffeine的特性,包括ConcurrentHashMap实现、多种淘汰机制(时间、权重、大小和引用)、加载模式(同步、异步及手动),以及淘汰通知。深入讲解了如何在实际项目中利用其与数据库结合优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.caffeine特点

Caffeine 内部使用ConcurrentHashMap实现,提供了多种缓存淘汰机制 (根据时间,根据权重,根据数量等),并且支持淘汰通知,而且Caffeine在保证线程安全的前提下缓存性能极高 被称为缓存之王

1.1官方性能比较

场景一:8个线程读,100%的读操作

场景二:6个线程读,2个线程写,也就是75%的读操作,25%的写操作

场景三:8个线程写,100%的写操作

可以清楚的看到Caffeine效率明显的高于其他缓存。

*1.2 caffeine内部结构图*

可以看到 caffeine内部采用ConcurrentHashMap进行缓存数据,并且提供了Scheduler机制(定时清除淘汰缓存),还有Executor(执行异步任务的线程池,在caffeine中主要用来执行异步查询任务并存入缓存)

2 如何使用Caffeine

*2.1 首先引入maven坐标*

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.1</version>
        </dependency>
​

*2.2 Caffeine的几种加载(put)模式*

*手动加载 代码如下*

    //手动加载
    @Test
    public void test() throws InterruptedException {
        // 初始化缓存,设置了100的缓存最大个数
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .build();
        int key1 = 1;
        // 使用getIfPresent方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null:
        System.out.println(cache.getIfPresent(key1));
        // 也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。如果缓存中不存在该 key
        // 则该函数将用于提供默认值,该值在计算后插入缓存中:
        System.out.println(cache.get(key1, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return 2;
            }
        }));
        cache.put(key1, 4);
        // 校验key1对应的value是否插入缓存中
        System.out.println(cache.getIfPresent(key1));
        // 移除数据,让数据失效
        cache.invalidate(key1);
​
        System.out.println(cache.getIfPresent(key1));
    }

打印结果

null
2
4
null

上面提到了两个get数据的方式,一个是getIfPercent,没数据会返回Null,而get数据的话则需要提供一个Function对象,当缓存中不存在查询的key则将该函数用于提供默认值,并且会插入缓存中 可以实现缓存中没有从其它地方查询的效果!

如果同时有多个线程进行get,那么这个Function对象是否会被执行多次呢?

实际上不会的,可以从结构图看出,Caffeine内部最主要的数据结构就是一个ConcurrentHashMap,而get的过程最终执行的便是ConcurrentHashMap.compute,这里仅会被执行一次。

实际应用:可以利用这个手动加载机制,也就是在Function对象的apply函数中,当从Caffeine缓存中取不到数据的时候则从数据库中读取数据,通过这个机制和数据库结合使用

同步加载

    
    @Test
    public void test() {
        // 初始化缓存,100的缓存最大个数
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumSize(100)
                .build(new CacheLoader<Integer, Integer>() {
                    @Nullable
                    @Override
                    public Integer load(@NonNull Integer key) {
                        return getInDB(key);
                    }
                });
​
        int key1 = 1;
        // get数据,取不到则从数据库中读取相关数据,该值也会插入缓存中:
        Integer value1 = cache.get(key1);
        System.out.println(value1);
        //get数据,如果取不到则执行function对象的apply方法返回
        System.out.println(cache.get(2, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer key) {
                return 888;
            }
        }));
​
        // 支持直接get一组值,支持批量查找
        Map<Integer, Integer> dataMap
                = cache.getAll(Arrays.asList(1, 2, 3));
        System.out.println(dataMap);
    }
​
    /**
     * 模拟从数据库中读取key
     *
     * @param key
     * @return
     */
    private int getInDB(int key) {
        return key + 1;
    }

打印结果

2
888
{1=2, 2=888, 3=4}

所谓的同步加载数据指的是,在get不到数据时最终会调用build构造时提供的CacheLoader对象中的load函数,如果返回值则将其插入缓存中,并且返回,这是一种同步的操作,也支持批量查找。

并且同步加载支持手动加载

实际应用:可以利用这个同步机制,也就是在CacheLoader对象中的load函数中,当从Caffeine缓存中取不到数据的时候则从数据库中读取数据,通过这个机制和数据库结合使用

异步加载

异步手动加载

    @Test
    public void test2() throws ExecutionException, InterruptedException {
        // 使用executor设置线程池
        AsyncCache<String, Integer> asyncCache = Caffeine.newBuilder()
                .maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();
        String key = "1";
        // get返回的是CompletableFuture
        CompletableFuture<Integer> future = asyncCache.get(key, new Function<String, Integer>() {
            @Override
            public Integer apply(String key) {
                return getValue(key);
            }
        });
        Integer value = future.get();
        System.out.println("当前所在线程:" + Thread.currentThread().getName());
        System.out.println(value);
    }
​
    private Integer getValue(String key) {
        System.out.println("异步线程:"+Thread.currentThread().getName());
        return Integer.valueOf(key)+1;
    }

执行结果如下

异步线程:pool-1-thread-1

当前所在线程:main

2

如上 在缓存中不存在时 会异步的执行getValue方法 并且把getValue获取到的值放到缓存中,可以看到getValue是在线程池提供的线程中执行的(如果不指定 采用默认的线程池),而且asyncCache.get()返回的是一个CompletableFuture,可以用CompletableFuture来实现异步串行并行的实现。

实际应用:可以利用这个异步机制,当从Caffeine缓存中多次获取数据,并且接版本发取不到数据并且从数据源中读取数据耗费的时间较长,通过这个机制和数据库结合使用

异步自动加载

    @Test
    public void test2() throws ExecutionException, InterruptedException {
        // 使用executor设置线程池
        AsyncLoadingCache<String, Integer> asyncCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync(key -> getValue(key));
        String key = "1";
        // get返回的是CompletableFuture
        CompletableFuture<Integer> future = asyncCache.get(key);
        Integer value = future.get();
        System.out.println("当前所在线程:" + Thread.currentThread().getName());
        System.out.println(value);
    }
​
    private Integer getValue(String key) {
        System.out.println("异步线程:"+Thread.currentThread().getName());
        return Integer.valueOf(key)+1;
    }

打印结果

异步线程:pool-1-thread-1
当前所在线程:main
2

跟同步加载一样 当缓存中没获取到数据 异步的从构建缓存时指定的getValue方法中获取

淘汰机制

caffeine最为实用之一的就是它健全的淘汰机制了,它提供了如下几种淘汰机制

  • 基于大小

  • 基于权重

  • 基于时间

  • 基于引用

基于大小淘汰

首先是基于大小淘汰,设置方式:maximumSize(个数),这意味着当缓存大小超过配置的大小限制时会发生回收。

    @Test
    public void test5() throws InterruptedException {
        // 初始化缓存,设置了1分钟的写过期,100的缓存最大个数
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumSize(1)
                .build();
        int key1 = 1;
        cache.get(key1, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return 2;
            }
        });
        int key2 = 2;
        cache.put(key2, 4);
        Thread.sleep(1000);
​
        System.out.println("缓存个数:"+cache.estimatedSize());
​
        System.out.println("缓存剩余值:"+cache.asMap().toString());
    }

打印结果

缓存个数:1
缓存剩余值:{2=4}

可以看到 在put进第二个元素之后 主线程休眠了一秒 这是因为上面提到的Scheduler机制,也就是淘汰缓存是一个异步的过程 并且基于大小淘汰遵循先入先出的原则,淘汰最先进入的值

基于权重淘汰

   
/**
     * 基于权重淘汰
     */
    @Test
    public void test6() throws InterruptedException {
        // 初始化缓存,设置了权重最大为4 权重计算为值的值
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumWeight(4)
                .weigher(new Weigher<Integer, Integer>() {
                    @Override
                    public int weigh(Integer key, Integer value) {
                        return value;
                    }
                })
                .build();
        int key1 = 1;
        cache.get(key1, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return 3;
            }
        });
        int key2 = 2;
        cache.put(key2,1);
​
        int key3 = 3;
        cache.put(key3,2);
​
        Thread.sleep(1000);
​
        System.out.println("缓存个数:"+cache.estimatedSize());
​
        System.out.println("缓存剩余值:"+cache.asMap().toString());
    }

输出结果

缓存个数:2
缓存剩余值:{2=1, 3=2}

可以看到 在上面我们设置了最大权重总和为4,每个缓存权重的计算方法就是值,当put一个3 再put一个1一个2的时候 总权重为5 所以将最开始的3淘汰了 这个时候总权重为3 小于4 所以最终结果为{2=1, 3=2} 当总权重大于最大权重时 权重淘汰法也遵循先入先出的淘汰方式

然后是基于时间的方式,基于时间的回收机制,Caffeine有提供了三种类型,可以分为:

  • 访问后到期,时间节点从最近一次读或者写,也就是get或者put开始算起。

  • 写入后到期,时间节点从写开始算起,也就是put。

  • 自定义策略,自定义具体到期时间。

访问后到期

    
    @Test
    public void test7() throws InterruptedException {
        // 初始化缓存,设置了1秒钟的读过期
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .build();
        int key1 = 1;
        // 则该函数将用于提供默认值,该值在计算后插入缓存中:
        System.out.println(cache.get(key1, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return 2;
            }
        }));
        Thread.sleep(2000);
        System.out.println(cache.getIfPresent(key1));
    }

打印结果

2
null

写入后到期

    
    @Test
    public void test8() throws InterruptedException {
        // 初始化缓存,设置了1秒钟的写过期
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .build();
        int key1 = 1;
        // 则该函数将用于提供默认值,该值在计算后插入缓存中:
        System.out.println(cache.get(key1, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return 2;
            }
        }));
        Thread.sleep(2000);
        System.out.println(cache.getIfPresent(key1));
    }

打印结果

2
null

自定义策略

    @Test
    public void test9() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder().expireAfter(new Expiry<Integer, Integer>() {
            //创建后一秒就过期  必须用纳秒
            @Override
            public long expireAfterCreate(@org.checkerframework.checker.nullness.qual.NonNull Integer k, @org.checkerframework.checker.nullness.qual.NonNull Integer v, long l) {
                return TimeUnit.SECONDS.toNanos(1);
            }
​
            //更新后二秒就过期  必须用纳秒
            @Override
            public long expireAfterUpdate(@org.checkerframework.checker.nullness.qual.NonNull Integer k, @org.checkerframework.checker.nullness.qual.NonNull Integer v, long l, @NonNegative long l1) {
                return TimeUnit.SECONDS.toNanos(2);
            }
​
            //访问后三秒就过期  必须用纳秒
            @Override
            public long expireAfterRead(@org.checkerframework.checker.nullness.qual.NonNull Integer k, @org.checkerframework.checker.nullness.qual.NonNull Integer v, long l, @NonNegative long l1) {
                return TimeUnit.SECONDS.toNanos(3);
            }
        }).scheduler(Scheduler.systemScheduler()).build();
​
        cache.put(1,1);
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent(1));
​
        cache.put(2,2);
        cache.put(2,2);
        Thread.sleep(1500);
        System.out.println(+cache.getIfPresent(2));
​
        cache.put(3,3);
        System.out.println(cache.getIfPresent(3));
        Thread.sleep(2000);
        System.out.println(cache.getIfPresent(3));
    }
    }

打印结果

null
2
3
3

可以看到 上文都设置了Scheduler ,如果没有设置Scheduler 那么淘汰的缓存就是在put或者get的时候才淘汰,也就是在我们操作数据的时候会进行异步清空过期数据

另外 如果使用上述的Scheduler.systemScheduler() 定时器 需要在java9的环境下才会生效,如果是java8 即使设置了Scheduler.systemScheduler() 也是在put或get时才去淘汰 如果是java8 需要使用指定的scheduler 例如 Scheduler.forScheduledExecutorService(Executors.newScheduledThreadPool(1))

引用淘汰

在说引用淘汰之前,先弄清引用的几种类型

  • 强引用 FinalReference

  • 软引用 SoftReference

  • 弱引用 WeakReference

  • 虚引用 PhantomReference

提供这四种引用的目的有

  • 方便Jvm进行垃圾回收

  • 方便开发人员使用,开发人员可以灵活的决定某些对象的生命周期

强引用

类似 Object object = new Object(); 这样的对象,只有对象存在变量引用 那么它就不会被jvm回收 如果内存满了的情况下 会抛出java.lang.OutOfMemoryError异常

    @Test
    public void test10(){
        Car car;
        List<Car> carList = new ArrayList<>();
        for(int i=0;i<5;i++){
            car = new Car("小车车");
            carList.add(car);
        }
    }
    class Car{
        public byte[] capacity;
​
        private String name;
​
        public Car(String name){
            //只是为了这个对象够大
            capacity = new byte[1024*1024*500];
            this.name = name;
        }
    }

如上所示 新创建的对象一直被list中的数组强引用 而且强引用的对象并不能被回收 所以最终堆内存溢出 抛出java.lang.OutOfMemoryError异常

强引用的对象如果需要被回收要把它置为null 例如 Car car = new Car("小车车"); car = null;

软引用

使用软引用包装的对象会在jvm内存不足的时候被回收掉

    
    @Test
    public void test11(){
        List<SoftReference<Car>> softReferenceList = new ArrayList<>();
        Car car;
        for(int i=0;i<5;i++){
            car = new Car("小车车");
            softReferenceList.add(new SoftReference<Car>(new Car("小车车"+i)));
        }
​
        for(SoftReference<Car> softReference : softReferenceList){
            System.out.println(softReference.get());
        }
    }

打印如下所示

null
null
Car{name='小车车2'}
Car{name='小车车3'}
Car{name='小车车4'}

可以看出在内存不足时 小车车0与小车车1被回收了

如上述 软引用可以使用在 一些使用次数较少的数据对象 如果内存不足 优先回收此类对象 避免内存溢出 待到需要使用时再次加载到堆中

弱引用

对象在被弱引用时 在gc回收时 不管内存是否充足 它都会被回收 代码如下

    @Test
    public void test12() throws InterruptedException {
        WeakReference<String> weakReference = new WeakReference<>(new String("小辣鸡"));
​
        System.out.println("gc前:"+weakReference.get());
​
        System.gc();
        Thread.sleep(1000);
​
        System.out.println("gc后:"+weakReference.get());
​
    }

打印如下

gc前:小辣鸡
gc后:null

虚引用

虚引用顾名思义就是形同虚设的引用 如果一个对象只存在虚引用 那么它就相当于没有任何引用 在任何时候都可能被gc回收

它的主要功能就是跟踪一个对象被垃圾回收的活动 需要与ReferenceQueue结合使用

ReferenceQueue

ReferenceQueue 引用其实也可以归纳为引用中的一员,可以和上述三种引用类型组合使用【软引用、弱引用、虚引用】。

在创建Reference时,手动将Queue注册到Reference中,而当该Reference所引用的对象被垃圾收集器回收时,JVM会将该Reference放到该队列中,而我们便可以对该队列做些其他业务,相当于一种通知机制。

例如下代码 将ReferenceQueue注册到虚引用(PhantomReference )创建时

    @Test
    public void test13() throws InterruptedException {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(new String("我在"),referenceQueue);

        System.out.println(phantomReference.get());

        System.gc();

        Thread.sleep(1000);
        Reference<?> reference;
        while((reference = referenceQueue.poll()) != null){
            if (reference == phantomReference){
                System.out.println("对象被回收了");
            }
        }
        System.out.println("结束");
    }

打印结果如下

null
对象被回收了
结束

弱引用淘汰机制

    @Test
    public void test14() throws InterruptedException {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                // 设置Key为弱引用,生命周期是下次gc的时候
                // 设置value为弱引用,生命周期是下次gc的时候
                .weakKeys().weakValues()
                .build(key->key);
        cache.put(new Integer(20),1);
        cache.put(new Integer(30),1);
        cache.put(new Integer(40),1);
        cache.put(new Integer(50),1);
        System.gc();
        Thread.sleep(8000);
        System.out.println(cache.estimatedSize());
    }

打印结果

0

上述例子缓存加入的值没有强引用 cache会将其包装为弱引用的对象 在gc时会将其回收 同样的 如果key没有强引用 也会包装成弱引用对象 都会在gc时被回收

软引用淘汰

    @Test
    public void test15() throws InterruptedException {
        Cache<Object, Object> cache = Caffeine.newBuilder()
                // 设置Key为弱引用,生命周期是下次gc的时候
                // 设置value为弱引用,生命周期是下次gc的时候
                .softValues()
                .build(key->key);
        for(int i = 0 ; i<8; i++){
            cache.put(1,new Car("小车车"+i));
        }
        System.out.println(cache.estimatedSize());
    }

打印结果

1 //可能为其它  根据你堆内存的大小

这里要注意的地方有三个

  • System.gc() 不一定会真的触发GC,只是一种通知机制,但是并非一定会发生GC,垃圾收集器进不进行GC是不确定的,所以有概率看到设置weakKeys了却在调用System.gc() 的时候却没有丢失缓存数据的情况。

  • 使用异步加载的方式不允许使用引用淘汰机制,启动程序的时候会报错:java.lang.IllegalStateException: Weak or soft values can not be combined with AsyncCache,猜测原因是异步加载数据的生命周期和引用淘汰机制的生命周期冲突导致的,因而Caffeine不支持。

  • 使用引用淘汰机制的时候,判断两个key或者两个value是否相同,用的是 ==,而非是equals(),也就是说需要两个key指向同一个对象才能被认为是一致的,这样极可能导致缓存命中出现预料之外的问题。

  • weakValues与softValues不能同时使用

刷新机制

如果想让缓存在写入后x秒失效 同时在写入后x秒之后访问数据时从数据源刷新该数据 可以采用caffeine的刷新机制

    int i = 1;
    //
    @Test
    public void test4() throws InterruptedException {
        // 设置写入后3秒后数据过期,2秒后如果有数据访问则刷新数据
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(3, TimeUnit.SECONDS)
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .build(new CacheLoader<Integer, Integer>() {
                    @Nullable
                    @Override
                    public Integer load(@NonNull Integer key) {
                        return getInDB();
                    }
                });
        cache.put(1, getInDB());
        // 休眠2.5秒,后取值
        Thread.sleep(2500);
        //注意 在此时返回的还是旧值  刷新后的值要下一次访问才生效
        System.out.println(cache.getIfPresent(1));
        // 休眠1.5秒,后取值
        Thread.sleep(1500);
        System.out.println(cache.getIfPresent(1));
    }
​
​
    private int getInDB() {
        // 这里为了体现数据被刷新,因而用了index++
        i++;
        return i;
    }
​

打印结果

2
3

淘汰通知回调

在业务中,我们可能会遇到某些缓存被淘汰之后需要进行业务操作 比如缓存被淘汰时更新到数据库,caffeine也提供了淘汰通知回调

    
@Test
    public void test17() throws InterruptedException {
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .scheduler(Scheduler.forScheduledExecutorService(Executors.newScheduledThreadPool(1)))
                // 增加了淘汰监听
                .removalListener(((key, value, cause) -> {
                    System.out.println("淘汰通知,key:" + key + ",原因:" + cause);
                }))
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable
                    Integer load(@NonNull Integer key) throws Exception {
                        return key;
                    }
                });
​
        cache.put(1, 2);
​
        Thread.sleep(3000);
        
    }

打印结果

淘汰通知,key:1,原因:EXPIRED

上述cause也就是淘汰原因有以下几种结果

  • EXPLICIT:如果原因是这个,那么意味着数据被我们手动的remove掉了。

  • REPLACED:就是替换了,也就是put数据的时候旧的数据被覆盖导致的移除。

  • COLLECTED:这个有歧义点,其实就是收集,也就是垃圾回收导致的,一般是用弱引用或者软引用会导致这个情况。

  • EXPIRED:数据过期,无需解释的原因。

  • SIZE:个数超过限制导致的移除。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值