所有分类
  • 所有分类
  • 未分类

Java本地缓存–Caffeine的用法

简介

本文介绍Java本地缓存工具Caffeine的用法。

常用的本地缓存方案有:

  1. JDK自带的(HashMap、ConcurrentHashMap等)
  2. Caffeine
  3. Guava Cache
  4. Encache

如果不需要超时时间,那么JDK的Map就够用了。

如果需要超时时间,就要使用第三方包了。推荐使用Caffeine,因为它性能很高。SpringBoot2.X及之后的版本使用的本地缓存就是Caffeine(旧版的SpringBoot用的是Guava Cache)。

快速上手

引入依赖

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
    <!-- 3.0.0及之后的版本不支持JDK8编译,需要更高版本的JDK才行 -->
</dependency>

使用

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.TimeUnit;

public class Demo {

    public static void main(String[] args) {
        // 创建一个Caffeine缓存,指定最大缓存大小为100
        Cache<String, Integer> cache = Caffeine.newBuilder()
                // 最大100,超过则放弃老的
                .maximumSize(100)
                // 10秒过期(没有读写自动删除)
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build();

        String key = "age";

        // 写入数据
        cache.put(key, 23);

        // 读取数据
        Integer value = cache.getIfPresent(key);
        System.out.println(value);

        // 删除
        cache.invalidate(key);

        // 读取数据
        value = cache.getIfPresent(key);
        System.out.println(value);
    }
}

结果

23
null

详细用法

填充策略

填充策略是指key不存在时如何创建一个对象进行返回,主要分为下面四种:

手动(Manual)

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Demo {

    public static void main(String[] args) {
        Cache<String, Integer> cache = Caffeine.newBuilder().build();

        String key = "age";

        // 读取数据
        Integer value = cache.get(key, k -> {
            System.out.println("key:" + k);
            return 18;
        });
        System.out.println(value);
    }
}

自动(Loading) 

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;

public class Demo {

    public static void main(String[] args) {
        //此时的类型是 LoadingCache 不是 Cache
        LoadingCache<String, Integer> cache = Caffeine.newBuilder().build(key -> {
            System.out.println("自动填充:" + key);
            return 18;
        });

        String key = "age";

        // 读取数据。key 不存在时会根据给定的CacheLoader自动装载进去
        Integer value = cache.get(key);
        System.out.println(value);
    }
}

结果

自动填充:age
18

异步手动(Asynchronous Manual)

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class Demo {

    public static void main(String[] args) throws Exception {
        //此时的类型是 LoadingCache 不是 Cache
        AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();

        String key = "age";

        //返回future对象, 调用其get方法会一直卡住直到得到返回,和多线程的submit一样
        CompletableFuture<Integer> ageFuture = cache.get(key, name -> {
            System.out.println("name:" + name);
            return 18;
        });

        Integer age = ageFuture.get();
        System.out.println("age:" + age);
    }
}

结果

name:age
age:18

异步自动(Asynchronously Loading) 

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;

import java.util.concurrent.CompletableFuture;

public class Demo {

    public static void main(String[] args) throws Exception {
        String key = "age";

        AsyncLoadingCache<String, Integer> cache = Caffeine.newBuilder().buildAsync(name -> {
            System.out.println("name:" + name);
            return 18;
        });
        CompletableFuture<Integer> ageFuture = cache.get(key);

        Integer age = ageFuture.get();
        System.out.println(age);
    }
}

结果

name:age
18

清除策略

有三种:基于大小、过期时间、引用类型。

1.基于大小

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Demo {

    public static void main(String[] args) throws Exception {
        Cache<String, Integer> cache = Caffeine.newBuilder()
                .maximumSize(10)
                .removalListener((key, value, cause) -> {
                    System.out.printf("key %s was removed(value:%s;cause:%s)\n", 
                            key, value, cause);
                })
                .build();

        for (int i = 0; i < 15; i++) {
            cache.put("name" + i, i);
        }

        Thread.currentThread().join();
    }
}

结果:

key name0 was removed(value:0;cause:SIZE)
key name3 was removed(value:3;cause:SIZE)
key name4 was removed(value:4;cause:SIZE)
key name2 was removed(value:2;cause:SIZE)
key name1 was removed(value:1;cause:SIZE)

也可以基于权重:

Cache<String, String> cache = Caffeine.newBuilder()
        .maximumWeight(1000)
        .weigher((String key, String value) -> value.length())
        .build();

2.过期时间

expireAfterAccess():缓存访问(读或写)后,一定时间失效;
expireAfterWrite():缓存写入后,一定时间失效;
expireAfter():自定义缓存策略,满足多样化的过期时间要求。

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;
import lombok.NonNull;
import org.checkerframework.checker.index.qual.NonNegative;

import java.util.concurrent.TimeUnit;

public class Demo {

    public static void main(String[] args) {
        // 1.缓存访问后(读或者写),一定时间后失效
        Cache<String, String> cache1 = Caffeine.newBuilder()
                .expireAfterAccess(10L, TimeUnit.SECONDS)
                .build();

        // 2.缓存写入后,一定时间后失效
        Cache<String, String> cache2 = Caffeine.newBuilder()
                .expireAfterWrite(10L, TimeUnit.SECONDS)
                .build();

        // 3.自定义过期策略
        Cache<String, String> cache3 = Caffeine.newBuilder()
                .expireAfter(new Expiry<Object, Object>() {
                    @Override
                    public long expireAfterCreate(@NonNull Object o, @NonNull Object o2, long l) {
                        return 0;
                    }

                    @Override
                    public long expireAfterUpdate(@NonNull Object o, @NonNull Object o2, long l, @NonNegative long l1) {
                        return 0;
                    }

                    @Override
                    public long expireAfterRead(@NonNull Object o, @NonNull Object o2, long l, @NonNegative long l1) {
                        return 0;
                    }
                })
                .build();
    }
}

3.引用类型

利用了Java中的引用类型,详见:这里

Caffeine.weakKeys():使用弱引用存储key,没有其他的强引用时,会被垃圾回收器回收;
Caffeine.weakValues():使用弱引用存储value,没有其他的强引用时,会被垃圾回收器回收;
Caffeine.softValues():使用软引用存储key,当没有其他的强引用时,内存不足的时候会被回收;
注:弱引用或软引用的值不能使用AsyncCache。

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Demo {

    public static void main(String[] args) {
        // 1.弱引用弱key方式,没有强引用时回收
        Cache<String, String> cache1 = Caffeine.newBuilder()
                .weakKeys()
                .weakValues()
                .build();

        // 2.软引用,内存不足的时回收
        Cache<String, String> cache2 = Caffeine.newBuilder()
                .softValues()
                .build();
    }
}

手动删除

Caffeine可以手动删除缓存,无需等待上面提到的被动的一些删除策略,方法如下:

cacheOne.invalidateAll();
cacheOne.invalidate(Object o);
cacheOne.invalidateAll(List);

监听移除事件

RemovalListener是缓存监听事件,key被移除时就会触发这个方法。RemovalListener可以获取到key、value和RemovalCause(删除的原因)。(RemovalListener中的操作是异步执行的)。

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class Demo {

    public static void main(String[] args) throws Exception {
        Cache<String, Integer> cache = Caffeine.newBuilder()
                .maximumSize(10)
                .removalListener((key, value, cause) -> {
                    System.out.printf("key %s was removed(value:%s;cause:%s)\n", 
                            key, value, cause);
                })
                .build();

        for (int i = 0; i < 15; i++) {
            cache.put("name" + i, i);
        }

        Thread.currentThread().join();
    }
}

结果

key name0 was removed(value:0;cause:SIZE)
key name3 was removed(value:3;cause:SIZE)
key name4 was removed(value:4;cause:SIZE)
key name2 was removed(value:2;cause:SIZE)
key name1 was removed(value:1;cause:SIZE)

监听写入操作

缓存读写都会调用到CacheWriter。

CacheWriter也可以代替上面提到的RemovalListener,但CacheWriter是同步执行的,而且是原子操作(写入缓存完成前会阻塞后续更新缓存的操作,但读缓存不会阻塞。

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheWriter;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.sun.istack.internal.Nullable;
import lombok.NonNull;

import java.util.concurrent.TimeUnit;

public class Demo {

    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(50, TimeUnit.SECONDS)
                .maximumSize(100)
                .writer(new CacheWriter<String, String>() {
                    @Override
                    public void write(@NonNull String key, @NonNull String value) {
                        // 持久化或者次级缓存
                        System.out.printf("---write: key:%s,value:%s%n", key, value);
                    }

                    @Override
                    public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause cause) {
                        // 从持久化或者次级缓存中删除
                        System.out.printf("---delete:key:%s,value:%s,cause:%s%n", key, value, cause);
                    }
                })
                .build();

        cache.put("test1", "value1");
        System.out.println(cache.asMap());

        cache.invalidate("test1");
        System.out.println(cache.asMap());
    }
}

结果

---write: key:test1,value:value1
{test1=value1}
---delete:key:test1,value:value1,cause:EXPLICIT
{}

统计

package com.knife.example.demo;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;

public class Demo {

    public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(1000)
                .recordStats()
                .build();
        
        cache.put("Hello", "Caffeine");
        
        for (int i = 0; i < 15; i++) {
            // 命中15次
            cache.getIfPresent("Hello");
        }
        
        for (int i = 0; i < 5; i++) {
            // 未命中5次
            cache.getIfPresent("a");
        }
        
        cache.get("b", key -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return key + "B";
        });
        
        CacheStats stats = cache.stats();
        System.out.println(stats);
        System.out.println("命中率:" + stats.hitRate());
        System.out.println("未命中率:" + stats.missRate());
        System.out.println("加载新值花费的平均时间:" + stats.averageLoadPenalty());
    }
}

 结果

CacheStats{hitCount=15, missCount=6, loadSuccessCount=1, loadFailureCount=0, totalLoadTime=1006854400, evictionCount=0, evictionWeight=0}
命中率:0.7142857142857143
未命中率:0.2857142857142857
加载新值花费的平均时间:1.0068544E9
0

评论0

请先

B站:远舟说编程,分享靠谱技术事儿
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录