写点什么

CaffeineCache Api 介绍以及与 Guava Cache 性能对比| 京东物流技术团队

  • 2024-05-17
    北京
  • 本文字数:11505 字

    阅读完需:约 38 分钟

一、简单介绍:

CaffeineCache 和 Guava 的 Cache 是应用广泛的本地缓存。


在开发中,为了达到降低依赖、提高访问速度的目的。会使用它存储一些维表接口的返回值和数据库查询结果,在有些场景下也会在分布式缓存上再加上一层本地缓存,用来减少对远程服务和数据库的请求次数。


CaffeineCache 是以 Guava Cache 为原型库开发和扩展的一种本地缓存,并且适配 Guava Cache 的 Api,但是 CaffeineCache 的性能更好。

二、CaffeineCache 的使用:

CaffeineCache 官方介绍有提供一些例子,不过这些例子不能直接运行。


下面围绕比较常用的 API 介绍下 CaffeineCache 的使用,列举一些可直接执行的 Demo,看起来明了一些。

1.存数据:

Caffeine 提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。

1.1 手动加载:

        Cache<String, String> cache = Caffeine.newBuilder()                 //过期时间                 .expireAfterWrite(10, TimeUnit.MINUTES)                 //最大容量                 .maximumSize(10_000)                .build();        String key = "test";        // 查找一个缓存元素, 没有查找到的时候返回null         String res = cache.get(key, k -> createValue(key));        // 添加或者更新一个缓存元素         cache.put(key, "testValue");         // 移除一个缓存元素        cache.invalidate(key);    }
// 模拟从外部数据源加载数据的逻辑 static String createValue(String key) { return "value"; }
复制代码


推荐使用 get(K var1, Function<? super K, ? extends V> var2);


get 方法可以在缓存中不存在该 key 对应的值时进行计算,生成并直接写入至缓存内,最后将结果返回,而当该 key 对应的值存在时将会直接返回值。


注意到 createValue 方法有可能会出现异常,根据官网所说:“当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null ”,那么实际情况怎么样呢?我们来试一下。


public class TestCaffeineCache {    public static void main(String[] args) {        Cache<String, String> cache = Caffeine.newBuilder()                .expireAfterWrite(10, TimeUnit.MINUTES)                .maximumSize(10_000)                .build();        String key = "test";        String res = cache.get(key, k -> createValue(key));        System.out.println(res);    }        // 模拟从外部数据源加载数据的逻辑     static String createValue(String key) {        //模拟异常情况        int a = 1/0;        return "";    }}
复制代码


运行结果:



可以看到,执行 cache.get 时,在生成结果的过程中如果出现异常了,cache.get 不会返回 null,仍会直接报错。

1.2 自动加载

public class TestCaffeineCache {    public static void main(String[] args) {         LoadingCache<String, String> cache = Caffeine.newBuilder()                .build(new CacheLoader<String, String>() {                    @Override                    public String load(String key) {                        return getValue(key);                    }                });
String value = cache.get("key"); System.out.println(value); }
// 模拟从外部数据源加载数据的逻辑 private static String getValue(String key) { // 实际情况下,这里会有从数据库、远程服务加载数据的逻辑 return "value"; }}
复制代码


可以用 lambda 简化它:


public class TestCaffeineCache {    public static void main(String[] args) {        LoadingCache<String, String> cache = Caffeine.newBuilder()                .build(key -> getValue(key));
String value = cache.get("key"); System.out.println(value); }
// 模拟从外部数据源加载数据的逻辑 private static String getValue(String key) { return "value"; }}
复制代码


上面的示例中, build 方法传入的 CacheLoader 定义了加载缓存的逻辑。调用 cache.get("key")时,如果缓存中不存在对应的值,CacheLoader 会调用 load 方法来加载和缓存值。


可以通过重写和 CacheLoader.load 和 loadAll 并手动调用,在 LoadingCache 创建之前提前加载一些数据。


    public static void main(String[] args) throws Exception {        CacheLoader loader = new CacheLoader<String,String>() {            @Override            public String load( String s) throws Exception {                return getValue(s);            }            @Override            public Map<String, String> loadAll(Iterable<? extends String> keys) throws Exception {                Map currentMap = new HashMap<String,String>();                for (String key : keys) {                    currentMap.put(key, getValue(key));                }                return currentMap;            }        };
loader.load("key1"); loader.loadAll(new ArrayList( Arrays.asList("key2","key3"))); LoadingCache<String, String> cache = Caffeine.newBuilder().build(loader); String value = cache.get("key1"); String value2 = cache.get("key2"); System.out.println(value+value2); }
// 模拟从外部数据源加载数据的逻辑 private static String getValue(String key) { return "value"; }
复制代码

1.3 手动异步加载:

    public static void main(String[] args) throws Exception {
AsyncCache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000).buildAsync(); String key ="test"; CompletableFuture<String> res = cache.get(key,k-> getValue(key)); res.thenAccept(result -> System.out.println(result)); }
// 模拟从外部数据源加载数据的逻辑 private static String getValue(String key) { return "value"; }
复制代码


异步加载使用的类是 AsyncCache,使用方法和 Cache 类似。cache.get(key, k -> getValue(key))将会返回一个 CompletableFuture,这一步骤会在一个异步任务中执行,而不会阻塞主线程。res.thenAccept 方法将在数据加载完成后得到结果。

1.4 自动异步加载:


public static void main(String[] args) throws Exception { Executor executor = Executors.newFixedThreadPool(5); AsyncLoadingCache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000) //异步的封装一段同步操作来生成缓存元素 .buildAsync(key -> getValue(key)) //OR建一个异步缓存元素操作并返回一个future .buildAsync((key,executor1) -> getValue(key,executor)); String key = "test"; CompletableFuture<String> res = cache.get(key); res.thenAccept(result -> System.out.println(result)); }
// 模拟从外部数据源加载数据的逻辑 private static CompletableFuture<String> getValue(String key,Executor executor) { return CompletableFuture.supplyAsync(() -> "value for " + key, executor); } private static String getValue(String key) { return "value"; }
复制代码


自动异步加载使用方法和手动异步加载类似,getValue 可接收一个 Executor 对象,用于自定义执行异步操作的线程池。

2.驱逐:

2.1 基于容量:

Cache<String, String> cache = Caffeine.newBuilder()                .maximumSize(10_000)                .build();
复制代码


最常用的驱逐策略了,Caffeine 提供多种算法根据最近使用频率和使用时间来驱逐元素 ref:Window-TinyLFU

2.2 基于权重:

class Product {    private String name;    private int weight;
public Product(String s, int i) { name=s; weight=i; } public String getName() { return name; } public int getWeight() { return weight; } @Override public String toString(){ return getName(); }}
public class TestCaffeineCache { public static void main(String[] args) { Cache<String, Product> cache = Caffeine.newBuilder() .maximumWeight(1000) .weigher((String key, Product value) -> value.getWeight()) //使用当前线程进行驱逐和刷新 .executor(runnable -> runnable.run()) //监听器,如果有元素被驱逐则会输出 .removalListener(((key, value, cause) -> { System.out.printf("Key %s was evicted (%s)%n", key, cause); })) .build(); // 向缓存中添加商品信息 cache.put("product1", new Product("Product 1", 200)); cache.put("product2", new Product("Product 2", 400)); cache.put("product3", new Product("Product 3", 500)); // 获取缓存中的商品信息 System.out.println(cache.getIfPresent("product1")); System.out.println(cache.getIfPresent("product2")); System.out.println(cache.getIfPresent("product3")); }}
复制代码



.weigher((String key, Product value) -> value.getWeight()) 制定了一个权重计算器,Product 对象的 getWeight()方法来计算权重。


通过示例中的返回结果可以看到,当 product3 被 put 后,总容量超过了 1000,product1 就被驱逐了。

2.3 基于时间:

附上官方的例子:


// 基于固定的过期时间驱逐策略LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfterAccess(5, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .expireAfterWrite(10, TimeUnit.MINUTES)    .build(key -> createExpensiveGraph(key));
// 基于不同的过期驱逐策略LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .expireAfter(new Expiry<Key, Graph>() { public long expireAfterCreate(Key key, Graph graph, long currentTime) { // Use wall clock time, rather than nanotime, if from an external resource long seconds = graph.creationDate().plusHours(5) .minus(System.currentTimeMillis(), MILLIS) .toEpochSecond(); return TimeUnit.SECONDS.toNanos(seconds); } public long expireAfterUpdate(Key key, Graph graph, long currentTime, long currentDuration) { return currentDuration; } public long expireAfterRead(Key key, Graph graph, long currentTime, long currentDuration) { return currentDuration; } }) .build(key -> createExpensiveGraph(key));
复制代码


Caffeine 提供了三种方法进行基于时间的驱逐——官方的解释:


expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。在当被缓存的元素时被绑定在一个 session 上时,当 session 因为不活跃而使元素过期的情况下,这是理想的选择。


expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。在对被缓存的元素的时效性存在要求的场景下,这是理想的选择。


expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。


写一个 Demo 举例 expireAfterAccess 和 expireAfterWrite:


    public static void main(String[] args) {        //模拟时间,使用的com.google.common.testing.FakeTicker;        FakeTicker ticker = new FakeTicker();        Cache<String, String> cache = Caffeine.newBuilder()                 //创建20分钟后元素被删除                .expireAfterWrite(20, TimeUnit.MINUTES)                 //没有读取10分钟后元素被删除                 .expireAfterAccess(10, TimeUnit.MINUTES)                .executor(Runnable::run)                .ticker(ticker::read)                .build();        cache.put("key1","value1");        cache.put("key2","value2");        ticker.advance(5, TimeUnit.MINUTES);        System.out.println("5分钟都不删除,访问一次key2:"+cache.getIfPresent("key2"));        ticker.advance(5, TimeUnit.MINUTES);        System.out.println("10分钟key1被删除,因为它已经10分钟没有被访问过了:"+cache.getIfPresent("key1"));        System.out.println("10分钟key2没有被删除,因为它在5分钟时被访问过了:"+cache.getIfPresent("key2"));        ticker.advance(10, TimeUnit.MINUTES);        System.out.println("20分钟key2也被删除:"+cache.getIfPresent("key2"));    }
复制代码


这个例子设定元素创建 20 分钟或者没有读取 10 分钟后被删除。


key1 和 key2 在创建后。5 分钟时访问一次 key2,十分钟时 key1 被删除,key2 没有被删除,20 分钟时 key2 也被删除。


运行结果正如我们期待的:



举例 expireAfter:


    public static void main(String[] args) {        //模拟时间,使用的com.google.common.testing.FakeTicker;        FakeTicker ticker = new FakeTicker();        Cache<String, String> cache = Caffeine.newBuilder()                .expireAfter(new Expiry<String, String>() {                    public long expireAfterCreate(String key, String value, long currentTime) {                        // 在创建后的24小时后过期                        return TimeUnit.HOURS.toNanos(24);                    }                    public long expireAfterUpdate(String key, String value, long currentTime, long currentDuration) {                        // 在更新后如果值为"1234",则立马过期                        if("1234".equals(value)){                            return 0;                        }                        // 在更新后的1小时后过期                        return TimeUnit.HOURS.toNanos(1);                    }                    public long expireAfterRead(String key, String value, long currentTime, long currentDuration) {                        // 在读取后的20小时后过期                        return TimeUnit.HOURS.toNanos(20);                    }                })                .executor(Runnable::run)                .ticker(ticker::read)                .build();        cache.put("AfterCreateKey","AfterCreate");        cache.put("AfterUpdate1234Key","1234key");        cache.put("AfterUpdateKey","AfterUpdate");        cache.put("AfterReadKey","AfterRead");        //AfterUpdate1234Key值更新为1234        cache.put("AfterUpdate1234Key","1234");        System.out.println("AfterUpdate1234Key在更新后值为1234,立马过期:"+cache.getIfPresent("AfterUpdate1234Key"));        System.out.println("AfterReadKey读取一次:"+cache.getIfPresent("AfterReadKey"));        //AfterUpdateKey更新一次        cache.put("AfterUpdateKey","AfterUpdate");        ticker.advance(1, TimeUnit.HOURS);        System.out.println("AfterUpdateKey更新了一个小时了,被删除:"+cache.getIfPresent("AfterUpdateKey"));        ticker.advance(19, TimeUnit.HOURS);        System.out.println("AfterReadKey再读取一次已经删除了,因为上一次读取已经过了20小时:"+cache.getIfPresent("AfterReadKey"));        ticker.advance(4, TimeUnit.HOURS);        System.out.println("AfterCreateKey被删除了,距离创建已经24小时了:"+cache.getIfPresent("AfterCreateKey"));    }
复制代码


这个例子设定了元素在以下四种情况会过期:


•创建后的 24 小时


•更新后值为"1234"


•更新后的 1 小时


•在读取后的 20 小时


以下是运行结果


2.4 基于引用:

基于引用的过期驱逐策略不常用,这里附上官方的例子和解释:


// 当key和缓存元素都不再存在其他强引用的时候驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()    .weakKeys()    .weakValues()    .build(key -> createExpensiveGraph(key));
// 当进行GC的时候进行驱逐LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .softValues() .build(key -> createExpensiveGraph(key));
复制代码


Caffeine 允许你配置缓存,以便 GC 去帮助清理缓存当中的元素,其中 key 支持弱引用,而 value 则支持弱引用和软引用。AsyncCache不支持软引用和弱引用。


Caffeine.weakKeys() 在保存 key 的时候将会进行弱引用。这允许在 GC 的过程中,当 key 没有被任何强引用指向的时候去将缓存元素回收。由于 GC 只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行 key 之间的比较。


Caffeine.weakValues()在保存 value 的时候将会使用弱引用。这允许在 GC 的过程中,当 value 没有被任何强引用指向的时候去将缓存元素回收。由于 GC 只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行 value 之间的比较。


Caffeine.softValues()在保存 value 的时候将会使用软引用。为了相应内存的需要,在 GC 过程中被软引用的对象将会被通过 LRU 算法回收。由于使用软引用可能会影响整体性能,我们还是建议通过使用基于缓存容量的驱逐策略代替软引用的使用。同样的,使用 softValues() 将会通过引用相等(==)而不是对象相等 equals()去进行 value 之间的比较。

3.移除:

3.1 移除的三个方法:

可以调用以下三个方法移除缓存中的元素


// 失效keyvoid invalidate(@CompatibleWith("K") @NonNull Object var1);// 批量失效keyvoid invalidateAll(@NonNull Iterable<?> var1);// 失效所有的keyvoid invalidateAll();
复制代码

3.2 监听器 removalListener 和 evictionListener

当元素从缓存中被移除时,这两个监听器可以进行指定的操作,具体有什么区别呢?先上例子:


        Cache<String, String> cache = Caffeine.newBuilder()                .maximumSize(2)                .executor(Runnable::run)                //驱逐,删除key时会输出                .removalListener(((key, value, cause) -> {                    System.out.printf("removalListener—>Key %s was evicted (%s)%n", key, cause);                }))                 //驱逐key时会输出                 .evictionListener(((key, value, cause) -> {                    System.out.printf("evictionListener->Key %s was evicted (%s)%n", key, cause);                }))                .build();        // 向缓存中添加商品信息        cache.put("product1", "product1");        cache.put("product2", "product2");        cache.put("product3", "product3");        // 获取缓存中的商品信息        System.out.println(cache.getIfPresent("product1"));        System.out.println(cache.getIfPresent("product2"));        System.out.println(cache.getIfPresent("product3"));        cache.invalidateAll();
复制代码


结果:



可以发现,当元素被驱逐,或者被手动移除时,removalListener 都会执行指定的操作。而 evictionListener 只会在元素被驱逐时执行指定的操作。

4.刷新:

4.1 自动刷新:

可以使用 refreshAfterWrite 在元素写入一段时间后刷新元素,先上代码


    public static void main(String[] args) {                FakeTicker ticker = new FakeTicker();        LoadingCache<String, String> cache = Caffeine.newBuilder()                .refreshAfterWrite(5, TimeUnit.SECONDS) // 在写入后5秒钟自动刷新                .ticker(ticker::read)                .executor(Runnable::run)                .build(key -> getVale(key)); // 提供加载方法
System.out.println("Initial value for key1: " + cache.get("key1"));
// 超过自动刷新时间 ticker.advance(7, TimeUnit.SECONDS);
System.out.println(cache.get("key1")); // 真正执行刷新 System.out.println(cache.get("key1")); // 输出自动刷新后的值 }
private static String getVale(String key) { // 这里简单地返回一个当前时间的字符串 return "loaded value for " + key + " at " + System.currentTimeMillis(); }
复制代码


输出结果:



可以发现过了刷新时间后,第一次访问 key1 并没有返回新值,第二次访问 key1 时才会将刷新后的数据返回,官方的解释是元素过了刷新时间不会立即刷新,而是在在访问时才会刷新,并且没有刷新完毕,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。

4.2 手动刷新:

可以使用 refresh(Key)方法进行手动刷新


    public static void main(String[] args) {        LoadingCache<String, String> cache = Caffeine.newBuilder()                .build(key -> getVale(key)); // 提供加载方法
System.out.println("Initial value for key1: " + cache.get("key1")); cache.refresh("key1"); System.out.println(cache.get("key1")); // 输出自动刷新后的值 }
private static String getVale(String key) { // 这里简单地返回一个当前时间的字符串 return "loaded value for " + key + " at " + System.currentTimeMillis(); }
复制代码


4.3 刷新自定义处理:

可以使用 CacheLoader.reload(K, V)来自定义刷新前后值的处理,下面这个例子重写了 reload 方法,将新值和旧值用"|"分开:


    public static void main(String[] args) {        FakeTicker ticker = new FakeTicker();        LoadingCache<String, String> cache = Caffeine.newBuilder()                .refreshAfterWrite(5, TimeUnit.SECONDS) // 在写入后5秒钟自动刷新                .ticker(ticker::read)                .build(new CacheLoader<String, String>() {                    @Override                    public String load(String s) throws Exception {                        return getVale(s);                    }                    //将刷新前后的数据都获取出来了                    @Override                    public String reload(String s,String v){                        return getVale(s)+"|"+v;                    }                }); // 提供加载方法        System.out.println("Initial value for key1: " + cache.get("key1"));        // 等待超过自动刷新时间        ticker.advance(7, TimeUnit.SECONDS);        cache.get("key1");        System.out.println(cache.get("key1")); // 输出自动刷新后的值    }
private static String getVale(String key) { // 这里简单地返回一个当前时间的字符串 return "loaded value for " + key + " at " + System.currentTimeMillis(); }
复制代码


结果:


三、性能对比:

我们知道,Caffeine 的性能比 Guava Cache 要好,可以写一个 demo 简单对比一下:

1.Caffeine Demo:

package test;
import com.github.benmanes.caffeine.cache.Cache;import com.github.benmanes.caffeine.cache.Caffeine;
public class CaffeineCacheTest {
public static void main(String[] args) throws Exception { Cache<Integer, Integer> loadingCache = Caffeine.newBuilder() .build();
// 开始时间 Long start = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { loadingCache.put(i, i); } // 存完成时间 Long writeFinishTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { loadingCache.getIfPresent(i); } // 读取完成时间 Long readFinishTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { loadingCache.invalidate(i); } // 删除完成时间 Long deleteFinishTime = System.currentTimeMillis(); System.out.println("CaffeineCache存用时:" + (writeFinishTime - start)); System.out.println("CaffeineCache读用时:" + (readFinishTime - writeFinishTime)); System.out.println("CaffeineCache删用时:" + (deleteFinishTime - readFinishTime)); }}
复制代码


运行结果:


2.Guava Cache Demo:

使用几乎一致的 API,换成 Guava Cache 再试一次:



package test;
import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;
public class GuavaCacheTest {
public static void main(String[] args) throws Exception { Cache<Integer, Integer> loadingCache = CacheBuilder.newBuilder() .build();
// 开始时间 Long start = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { loadingCache.put(i, i); } // 存完成时间 Long writeFinishTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { loadingCache.getIfPresent(i); } // 读取完成时间 Long readFinishTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { loadingCache.invalidate(i); } // 删除完成时间 Long deleteFinishTime = System.currentTimeMillis(); System.out.println("GuavaCache存用时:"+(writeFinishTime-start)); System.out.println("GuavaCache取用时:"+(readFinishTime-writeFinishTime)); System.out.println("GuavaCache删用时:"+(deleteFinishTime-readFinishTime)); }
}
复制代码


运行结果:


3.多组测试结果:

运行环境:处理器:Apple M3 ,内存:18 GB,JDK1.8


更改循环次数,多组测试结果如下(单位 ms):



可以看出 Caffeine 的总体性能是比 Guava Cache 要好的。


当然,基于本地单机的简单测试,结果受处理器,线程,内存等影响较大。可以参考下官方的测试,有更高的参考意义:官方测试。

四、总结:

本文举了很多的例子,介绍了 Caffeine 支持的多种基础的操作,包括存、取、删等。以及异步、监听、刷新等更多拓展的操作,能够覆盖大部分需要本地缓存的开发场景。


Caffeine 的性能比 Guava Cache 更好,并列举了一个性能测试 demo,Caffeine 兼容 Guava Cache 的 API,所以从 Guava Cache 迁移至 Caffeine 也比较容易。


最后附上 Caffeine 的官方网址:官方网址(中文)。


作者:京东物流 殷世杰


来源:京东云开发者社区

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
CaffeineCache Api介绍以及与Guava Cache性能对比| 京东物流技术团队_京东科技开发者_InfoQ写作社区