当前位置: 首页 > 工具软件 > Caffeine > 使用案例 >

Caffeine使用分享

南宫森
2023-12-01

缓存分类

        缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等

Caffeine简介

        Caffeine是一个基于Java8开发的高性能Java缓存库,可提供接近最佳的命中率,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。缓存与ConcurrentMap相似,但并不完全相同。最根本的区别是ConcurrentMap会保留添加到其中的所有元素,直到将其明确删除为止。

Caffeine可以通过建造者模式灵活的组合以下特性:

  1. 通过异步自动加载实体到缓存中
  2. 基于大小的回收策略
  3. 基于时间的回收策略
  4. 自动刷新
  5. key自动封装虚引用
  6. value自动封装弱引用或软引用
  7. 实体过期或被删除的通知
  8. 写入外部资源
  9. 统计累计访问缓存

如何使用

在项目pom.xml中添加Caffeine的依赖

 <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
 <dependency>
     <groupId>com.github.ben-manes.caffeine</groupId>
     <artifactId>caffeine</artifactId>
     <version>2.8.6</version>
 </dependency>

缓存加载

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

手动加载

Caffeine 有两种方式限制缓存大小。两种配置互斥,不能同时配置

  1. 创建一个限制容量 Cache
Cache<String, String> cache = Caffeine.newBuilder()
 		// 设置超时时间为5s,超过该时间缓存过期
        .expireAfterWrite(5, TimeUnit.SECONDS)
        // 设置缓存最大条目数,超过条目则触发回收。
        .maximumSize(1)
        .build();

需要注意的是,实际实现上为了性能考虑,这个限制并不会很死板:

  • 在缓存元素个数快要达到最大限制的时候,过期策略就开始执行了,所以在达到最大容量前也许某些不太可能再次访问的 Entry (Key-Value)就被过期掉了。
  • 有时候因为过期 Entry 任务还没执行完,更多的 Entry 被放入缓存,导致缓存的 Entry 个数短暂超过了这个限制

示例:

 /**
 * 手动加载cache
 */
@Test
public void testManualLoadCache2() throws InterruptedException {
    Cache<String, String> cache = Caffeine.newBuilder()
    		// 设置超时时间为5s,超过该时间缓存过期
            .expireAfterWrite(5, TimeUnit.SECONDS) 
            // 设置缓存最大条目数,超过条目则触发回收。
            .maximumSize(1)
            .build();
    // 查找一个缓存元素, 没有查找到的时候返回null
    String value = cache.getIfPresent("test");
    //执行结果--> null
    System.out.println(value);
    // 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
    value = cache.get("test", k -> "test-value");
    //执行结果--> test-value
    System.out.println(cache.getIfPresent("test"));
    //执行结果--> test-value
    System.out.println(value);


    // 加入一些缓存数据
    List<String> list = new ArrayList<>();
    for (int i = 2; i < 10; i++) {
        list.add("test" + i);
    }
    for (int i = 2; i < 10; i++) {
        // 添加或者更新一个缓存元素
        cache.put("test" + i, "test-value" + i);
    }

    // 执行缓存回收
    // 缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用cache.cleanUp()方法手动触发一次回收操作。cache.cleanUp()是一个同步方法。
    cache.cleanUp();

    //根据key list去获取一个map的缓存
    Map<String, String> dataObjectMap
            = cache.getAllPresent(list);
    //查看缓存中的数据
    //执行结果--> 1
    System.out.println(dataObjectMap.size()); 
    //执行结果--> {test9=test-value9}
    System.out.println(dataObjectMap); 
	//设置10s的睡眠时间,使得超过过期时间
    Thread.sleep(10000); 
    //执行结果--> null
    System.out.println(cache.getIfPresent("test"));
}
  1. 创建一个自定义权重限制容量的 Cache
 Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
                //最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
                .maximumWeight(1)
                //每个 Entry 的 weight 值
                .weigher(new Weigher<String, List<Object>>() {
                    @Override
                    public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
                        return value.size();
                    }
                })
                .build();

上面我们的 value 是一个 list,以 list 的大小作为 Entry 的大小。当把 Weigher 实现为只返回1,maximumWeight 其实和 maximumSize 是等效的。 同样的,为了性能考虑,这个限制也不会很死板。

示例:

@Test
public void testManualLoadCache4() {
    Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
            //最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
            .maximumWeight(1)
            //每个 Entry 的 weight 值
            .weigher(new Weigher<String, List<Object>>() {
                @Override
                public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
                    return value.size();
                }
            })
            .build();

    stringListCache.put("test1", Collections.singletonList("test-value1"));
    stringListCache.put("test2", Arrays.asList("test-value2","test-value2"));


    stringListCache.cleanUp();

    Map<String, List<Object>> dataObjectMap = stringListCache.getAllPresent(Arrays.asList("test1","test2"));
    System.out.println(dataObjectMap.size()); // --> 1
    System.out.println(dataObjectMap); // --> {test1=[test-value1]}
}
  1. 指定初始大小
Cache<String, Object> cache = Caffeine.newBuilder()
    //指定初始大小
    .initialCapacity(1000)
    .build();

HashMap类似,通过指定一个初始大小,减少扩容带来的性能损耗。这个值也不宜过大,浪费内存。

自动加载

  • 创建LoadingCache

示例:

	@DisplayName("测试LoadingCache")
    @Test
    public void testLoadingCache() {
        LoadingCache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(10_000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public @Nullable String load(@NonNull String key) throws Exception {
                        //默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
                        System.out.println("load data --- " + key);
                        //模拟从数据库中获取数据
                        return MAP.get(key);
                    }
                });
        //第一次的时候会调用load方法
        System.out.println(cache.get("test1")); 
        //第二次不会调用load方法
        System.out.println(cache.get("test1")); 
    }

手动异步加载

  • 创建AsyncCache

示例:

	 @Test
    public void testAsyncCache() throws ExecutionException, InterruptedException {
        AsyncCache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10_000)
                .buildAsync();

        // 查找缓存元素,如果不存在,则异步生成
        CompletableFuture<String> value = cache.get("test1", k -> {
            //异步加载
            System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
            System.out.println("load cache ---" + k);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return MAP.get(k);
        });
        System.out.println(Thread.currentThread().getName()); //main
        System.out.println("=========");
        System.out.println(value.get()); //value1, 阻塞
    }

测试结果:

ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1

自动异步加载

  • 创建AsyncLoadingCache

示例1:

	@Test
    public void testAsynchronouslyLoadingCache() throws ExecutionException, InterruptedException {
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10_000)
                //异步的封装一段同步操作来生成缓存元素
                .buildAsync(new CacheLoader<String, String>() {
                    @Override
                    public @Nullable String load(@NonNull String key) throws Exception {
                        System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
                        System.out.println("load cache ---" + key);
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return MAP.get(key);
                    }
                });

        CompletableFuture<String> value = cache.get("test1");
        // main
        System.out.println(Thread.currentThread().getName()); 
        System.out.println("=========");
        // value1 阻塞
        System.out.println(value.get()); 
    }

测试结果

ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1

示例2:

	@Test
    public void testAsynchronouslyLoadingCache2() throws ExecutionException, InterruptedException {
        AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10_000)
                //构建一个异步缓存元素操作并返回一个future
                .buildAsync(new AsyncCacheLoader<String, String>() {
                    @Override
                    public @NonNull CompletableFuture<String> asyncLoad(@NonNull String key, @NonNull Executor executor) {
                        System.out.println(Thread.currentThread().getName()); //main
                        return CompletableFuture.supplyAsync(() -> {
                            System.out.println("load cache");
                            try {
                                Thread.sleep(2000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            // ForkJoinPool.commonPool-worker-3                            
                            System.out.println(Thread.currentThread().getName()); 
                            return MAP.get(key);
                        });
                    }

                });

        System.out.println(cache.get("test1").get()); // value1 阻塞
    }

测试结果:

main
load cache
ForkJoinPool.commonPool-worker-3
value1

过期策略

基于大小

基于大小的我们前面已经讲到了。也就是通过设置maximumSize来进行大小驱逐策略,还有设置maximumWeight来设置权重驱逐策略

示例:

	@Test
    public void testManualLoadCache6() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(1)
                .build();
        cache.put("key1","value1");
        cache.put("key2","value2");

        System.out.println(cache.getIfPresent("key1"));
        System.out.println(cache.getIfPresent("key2"));

        cache.cleanUp();

        System.out.println(cache.getIfPresent("key1"));
        System.out.println(cache.getIfPresent("key2"));
    }

基于时间

Caffeine提供了三种定时驱逐策略

  1. expireAfterWrite(long, TimeUnit)
  • 在最后一次写入缓存后开始计时,在指定的时间后过期。

示例:

	@DisplayName("基于时间的过期策略,设置expireAfterWrite")
    @Test
    public void testManualLoadCache7() throws InterruptedException {
        //在最后一次写入缓存后开始计时,在指定的时间后过期。
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(3,TimeUnit.SECONDS)
                .build();
        cache.put("key1","value1");
        Thread.sleep(1000);
        //执行结果-> value1
        System.out.println(cache.getIfPresent("key1")); 
        Thread.sleep(1000);
        //执行结果-> value1
        System.out.println(cache.getIfPresent("key1")); 
        Thread.sleep(1000);
        //执行结果-> null
        System.out.println(cache.getIfPresent("key1")); 
    }

        caffeine的缓存清除策略是惰性删除定时删除,如果想使用缓存作为一个定时容器,缓存移除监听当作定时器的回调,可能会出现下面的问题:即使key已过期,若没有对该key再次访问,则该key将一直不会被删除,移除监听里的代码永远得不到执行。

  1. expireAfterAccess(long, TimeUnit)
  • 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。

示例:

	@DisplayName("基于时间的过期策略,设置expireAfterAccess")
    @Test
    public void testManualLoadCache8() throws InterruptedException {
        // 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterAccess(3,TimeUnit.SECONDS)
                .build();
        cache.put("key1","value1");
        Thread.sleep(1000);
        //执行结果-> value1
        System.out.println(cache.getIfPresent("key1")); 
        Thread.sleep(1000);
        //执行结果-> value1
        System.out.println(cache.getIfPresent("key1")); 
        Thread.sleep(1000);
        //执行结果-> value1
        System.out.println(cache.getIfPresent("key1")); 
        Thread.sleep(3001);
        //执行结果-> null
        System.out.println(cache.getIfPresent("key1")); 
    }
  1. expireAfter(Expiry)
  • 在expireAfter中需要自己实现Expiry接口,这个接口支持expireAfterCreate,expireAfterUpdate,以及expireAfterRead了之后多久过期。注意这个是和expireAfterAccess、expireAfterAccess是互斥的。这里和expireAfterAccess、expireAfterAccess不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,获取具体的过期时间。

示例:

	@DisplayName("基于时间的过期策略,设置expireAfterCreate")
    @Test
    public void testManualLoadCache9() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        return TimeUnit.SECONDS.toNanos(3);
                    }

                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentDuration;
                    }

                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentDuration;
                    }
                })
                .build();
        cache.put("key1", "value1");
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value1
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value1
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1"));  // -> null
    }


    @DisplayName("基于时间的过期策略,设置expireAfterUpdate")
    @Test
    public void testManualLoadCache10() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        return Long.MAX_VALUE;
                    }

                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return TimeUnit.SECONDS.toNanos(3);
                    }

                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentDuration;
                    }
                })
                .build();
        cache.put("key1", "value1");
        cache.put("key1", "value2");
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value2
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value2
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1"));  // -> null
    }


    @DisplayName("基于时间的过期策略,设置expireAfterRead")
    @Test
    public void testManualLoadCache11() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        return Long.MAX_VALUE;
                    }

                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentDuration;
                    }

                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return TimeUnit.SECONDS.toNanos(3);
                    }
                })
                .build();
        cache.put("key1", "value1");
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value1
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value1
        Thread.sleep(1000);
        System.out.println(cache.getIfPresent("key1")); // -> value1
        Thread.sleep(3001);
        System.out.println(cache.getIfPresent("key1")); // -> null
    }

基于引用

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

Caffeine.weakKeys()

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

示例:

	/**
     * Caffeine.weakKeys() 在保存key的时候将会进行弱引用。
     * 这允许在GC的过程中,当key没有被任何强引用指向的时候去将缓存元素回收。
     * 由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行key之间的比较。
     */
    @Test
    public void testWeakKeys() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .weakKeys()
                .build();
        cache.put(new String("test"), "value1");
        System.out.println(cache.asMap());//{test=value1}
        System.gc();
        System.out.println(cache.asMap()); //value1
    }
Caffeine.weakValues()

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

示例:

	/**
     * Caffeine.weakValues()在保存value的时候将会使用弱引用。
     * 这允许在GC的过程中,当value没有被任何强引用指向的时候去将缓存元素回收。
     * 由于GC只依赖于引用相等性。这导致在这个情况下,缓存将会通过引用相等(==)而不是对象相等 equals()去进行value之间的比较。
     */
    @Test
    public void testWeakValues() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .weakKeys()
                .weakValues()
                .build();
        cache.put("test1", new String("value"));
        System.out.println(cache.asMap());//{test1=value}
        System.out.println(cache.getIfPresent("test1")); //value
        System.gc();
        System.out.println(cache.getIfPresent("test1")); //null
        System.out.println(cache.asMap());//{}
    }
Caffeine.softValues()

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

示例:

	@Test
    public void testWithoutSoftValues() {
        Cache<String, byte[]> cache = Caffeine.newBuilder()
                .build();
        cache.put("test1", new byte[1024 * 1024 * 1024]);
        cache.put("test2", new byte[1024 * 1024 * 1024]);
        cache.put("test3", new byte[1024 * 1024 * 1024]);
        cache.put("test4", new byte[1024 * 1024 * 1024]);
        //Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        System.out.println(cache.asMap());
    }
	 @Test
    public void testSoftValues() {
        Cache<String, byte[]> cache = Caffeine.newBuilder()
                .softValues()
                .build();
        cache.put("test1", new byte[1024 * 1024 * 1024]);
        cache.put("test2", new byte[1024 * 1024 * 1024]);
        cache.put("test3", new byte[1024 * 1024 * 1024]);
        cache.put("test4", new byte[1024 * 1024 * 1024]);
        System.out.println(cache.asMap());//{test4=[B@5bf0d49}
    }

如果不使用softValues的话,程序会报OutOfMemoryError,如果使用了softValues则会回收掉缓存

缓存移除

invalidate(Object key)方法

示例:

	@DisplayName("测试移除cache,invalidate(key)方法")
    @Test
    public void testRemoveCache() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(50, TimeUnit.SECONDS)
                .maximumSize(100)
                .build();

        cache.put("test1","value1");
        cache.put("test2","value2");
        cache.put("test3","value3");
        cache.put("test4","value4");

        System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
        cache.invalidate("test1"); //移除指定key的Entry
        System.out.println(cache.asMap()); //{test4=value4, test2=value2, test3=value3}
    }

invalidateAll(Iterable<?> keys)方法

示例:

 	@DisplayName("测试移除cache,invalidateAll(keys)方法")
    @Test
    public void testRemoveCache3() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(50, TimeUnit.SECONDS)
                .maximumSize(100)
                .build();

        cache.put("test1","value1");
        cache.put("test2","value2");
        cache.put("test3","value3");
        cache.put("test4","value4");

        System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
        cache.invalidateAll(Arrays.asList("test1","test2")); //批量移除指定key的Entry
        System.out.println(cache.asMap()); //{test4=value4, test3=value3}
    }

invalidateAll()方法

示例:

	@DisplayName("测试移除cache,invalidateAll()方法")
    @Test
    public void testRemoveCache2() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(50, TimeUnit.SECONDS)
                .maximumSize(100)
                .build();

        cache.put("test1","value1");
        cache.put("test2","value2");
        cache.put("test3","value3");
        cache.put("test4","value4");

        System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
        cache.invalidateAll(); //移除所有的cache
        System.out.println(cache.asMap()); //{}
    }

移除监听器(RemovalListener)

示例:

    @Test
    public void testRemovalListener() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(50, TimeUnit.SECONDS)
                .maximumSize(100)
                .removalListener((RemovalListener<String, String>) (key, value, cause) -> System.out.println(Thread.currentThread().getName() + "--" + MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]",key,value,cause)))
                .build();

        cache.put("test1", "value1");
        cache.put("test2", "value2");
        cache.put("test3", "value3");
        cache.put("test4", "value4");


        System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
        cache.invalidate("test1"); //移除指定key的Entry
        System.out.println(cache.asMap()); //{test4=value4, test2=value2, test3=value3}
        //removalListener打印:ForkJoinPool.commonPool-worker-3--key:[test1],value:[value1],cause:[EXPLICIT]
    }


    @Test
    public void testRemovalListener2() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(50, TimeUnit.SECONDS)
                .maximumSize(1)
                .removalListener((RemovalListener<String, String>) (key, value, cause) -> System.out.println(MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]",key,value,cause)))
                .build();

        cache.put("test1", "value1");
        cache.put("test2", "value2");

        System.out.println(cache.asMap()); //{test1=value1, test4=value4, test2=value2, test3=value3}
        cache.cleanUp();
        System.out.println(cache.asMap()); //{test4=value4, test2=value2, test3=value3}
        //removalListener打印:key:[test1],value:[value1],cause:[SIZE]
    }


    @Test
    public void testRemovalListener3() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .maximumSize(10)
                .removalListener((RemovalListener<String, String>) (key, value, cause) -> System.out.println(MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]",key,value,cause)))
                .build();

        cache.put("test1", "value1");
        System.out.println(cache.asMap()); //{test1=value1}
        Thread.sleep(1000);
        cache.cleanUp();
        System.out.println(cache.asMap()); //{}
        //removalListener打印:key:[test1],value:[value1],cause:[EXPIRED]
    }

Writer

我们还可以通过设置 Writer,将对于缓存的更新,作用于其他存储,例如数据库。

示例:

    @Test
    public void testWriter() {
        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.println(MessageFormat.format("key:[{0}],value:[{1}]", key, value));
                    }

                    @Override
                    public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause cause) {
                        // 从持久化或者次级缓存中删除
                        System.out.println(MessageFormat.format("key:[{0}],value:[{1}],cause:[{2}]", key, value, cause));
                    }
                })
                .build();

        cache.put("test1", "value1");
        cache.put("test2", "value2");

        System.out.println("===========");

        System.out.println(cache.asMap());
        cache.invalidate("test1");
        System.out.println(cache.asMap());
        cache.put("test2", "value222");

        /**
         * 打印结果:
         * key:[test1],value:[value1]
         * key:[test2],value:[value2]
         * ===========
         * {test1=value1, test2=value2}
         * key:[test1],value:[value1],cause:[EXPLICIT]
         * {test2=value2}
         * key:[test2],value:[value222]
         */
    }

统计

通过使用Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个CacheStats对象,其将会含有一些统计指标,比如:

  • hitRate(): 查询缓存的命中率
  • evictionCount(): 被驱逐的缓存数量
  • averageLoadPenalty(): 新值被载入的平均耗时

示例:

    @Test
    public void testRecordStats() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(1)
                //自定义数据采集器
                .recordStats(() -> new StatsCounter() {
                    @Override
                    public void recordHits(@NonNegative int count) {
                        System.out.println("recordHits:" + count);
                    }

                    @Override
                    public void recordMisses(@NonNegative int count) {
                        System.out.println("recordMisses:" + count);
                    }

                    @Override
                    public void recordLoadSuccess(@NonNegative long loadTime) {
                        System.out.println("recordLoadSuccess:" + loadTime);
                    }

                    @Override
                    public void recordLoadFailure(@NonNegative long loadTime) {
                        System.out.println("recordLoadFailure:" + loadTime);
                    }

                    @Override
                    public void recordEviction() {
                        System.out.println("recordEviction...");
                    }

                    @Override
                    public @NonNull CacheStats snapshot() {
                        return null;
                    }
                })
                .build();
        cache.put("test1", "value1");
        cache.put("test2", "value2");
        System.out.println(cache.asMap());
        cache.getIfPresent("test1");
        cache.getIfPresent("test3");
        cache.cleanUp();
        System.out.println(cache.asMap());
        /**
         * 打印结果:
         * {test1=value1, test2=value2}
         * recordHits:1
         * recordMisses:1
         * recordEviction...
         * {test2=value2}
         */
    }


    @Test
    public void testRecordStats2() {
        LoadingCache<String, String> asyncCache = Caffeine.newBuilder()
                .maximumSize(1)
                //自定义数据采集器
                .recordStats(() -> new StatsCounter() {
                    @Override
                    public void recordHits(@NonNegative int count) {
                        System.out.println("recordHits:" + count);
                    }

                    @Override
                    public void recordMisses(@NonNegative int count) {
                        System.out.println("recordMisses:" + count);
                    }

                    @Override
                    public void recordLoadSuccess(@NonNegative long loadTime) {
                        System.out.println("recordLoadSuccess:" + loadTime);
                    }

                    @Override
                    public void recordLoadFailure(@NonNegative long loadTime) {
                        System.out.println("recordLoadFailure:" + loadTime);
                    }

                    @Override
                    public void recordEviction() {
                        System.out.println("recordEviction...");
                    }

                    @Override
                    public @NonNull CacheStats snapshot() {
                        return null;
                    }
                })
                .build(new CacheLoader<String, String>() {
                    @Override
                    public @Nullable String load(@NonNull String key) throws Exception {
                        return MAP.get(key);
                    }
                });

        asyncCache.get("test1");
        System.out.println(asyncCache.asMap());

        /**
         * 打印:
         * recordMisses:1
         * recordLoadSuccess:19800
         * {test1=value1}
         */
    }


    @Test
    public void testRecordStats3() {
        LoadingCache<String, String> asyncCache = Caffeine.newBuilder()
                .maximumSize(1)
                //自定义数据采集器
                .recordStats(() -> new StatsCounter() {
                    @Override
                    public void recordHits(@NonNegative int count) {
                        System.out.println("recordHits:" + count);
                    }

                    @Override
                    public void recordMisses(@NonNegative int count) {
                        System.out.println("recordMisses:" + count);
                    }

                    @Override
                    public void recordLoadSuccess(@NonNegative long loadTime) {
                        System.out.println("recordLoadSuccess:" + loadTime);
                    }

                    @Override
                    public void recordLoadFailure(@NonNegative long loadTime) {
                        System.out.println("recordLoadFailure:" + loadTime);
                    }

                    @Override
                    public void recordEviction() {
                        System.out.println("recordEviction...");
                    }

                    @Override
                    public @NonNull CacheStats snapshot() {
                        return null;
                    }
                })
                .build(new CacheLoader<String, String>() {
                    @Override
                    public @Nullable String load(@NonNull String key) throws Exception {
                        throw new RuntimeException("failed");
                    }
                });

        asyncCache.get("test1");
        System.out.println(asyncCache.asMap());

        /**
         * 打印:
         * recordMisses:1
         * recordLoadFailure:41100
         */
    }


    @Test
    public void testRecordStats4() {
        Cache<String, String> cache = Caffeine.newBuilder()
                .maximumSize(1)
                //打开数据采集
                .recordStats()
                .build();
        cache.put("test1", "value1");
        cache.put("test2", "value2");
        System.out.println(cache.asMap());//{test1=value1, test2=value2}
        cache.getIfPresent("test1");
        cache.getIfPresent("test3");
        cache.cleanUp();
        System.out.println(cache.asMap());//{test2=value2}
        System.out.println(cache.stats().hitRate());//查询缓存的命中率 0.5
        System.out.println(cache.stats().hitCount());//命中次数 1
        System.out.println(cache.stats().evictionCount());//被驱逐的缓存数量 1
        System.out.println(cache.stats().averageLoadPenalty());//新值被载入的平均耗时
        /**
         * 打印结果:
         * {test1=value1, test2=value2}
         * {test2=value2}
         * 0.5
         * 1
         * 1
         * 0.0
         */
    }


    @Test
    public void testRecordStats5() {
        LoadingCache<String, String> asyncCache = Caffeine.newBuilder()
                .maximumSize(1)
                //打开数据采集
                .recordStats()
                .build(new CacheLoader<String, String>() {
                    @Override
                    public @Nullable String load(@NonNull String key) throws Exception {
                        return MAP.get(key);
                    }
                });

        asyncCache.get("test1");
        asyncCache.get("test1");
        System.out.println(asyncCache.asMap());//{test1=value1}
        System.out.println(asyncCache.stats().hitRate());//查询缓存的命中率 0.5
        System.out.println(asyncCache.stats().hitCount());//命中次数 1
        System.out.println(asyncCache.stats().evictionCount());//被驱逐的缓存数量 0
        System.out.println(asyncCache.stats().averageLoadPenalty());//新值被载入的平均耗时 21100.0
        /**
         * 打印:
         * {test1=value1}
         * 0.5
         * 1
         * 0
         * 21100.0
         */
    }

Guava迁移

那么,如果我的项目之前用的是GuavaCache,如何以尽可能低的成本迁移到Caffeine上来呢?嘿嘿,Caffeine已经想到了这一点,它提供了一个适配器,让你用Guava的接口操作它的缓存。代码片段如下所示:

// Guava's LoadingCache interface
LoadingCache<Key, Graph> graphs = CaffeinatedGuava.build(
    Caffeine.newBuilder().maximumSize(10_000),
    new CacheLoader<Key, Graph>() { // Guava's CacheLoader
        @Override public Graph load(Key key) throws Exception {
          return createExpensiveGraph(key);
        }
    });

参考

Caffeine官方Github地址

关于淘汰算法的经典文章:Design Of A Modern Cache

SpringBoot 使用 Caffeine 本地缓存

深入解密来自未来的缓存-CaffeineCaffeine官方API文档

Caffeine官方API文档

 类似资料: