文章目录

  • Java高性能本地缓存框架Caffeine
  • 如何使用
  • 缓存加载
  • 手动加载
  • 自动加载
  • 手动异步加载
  • 自动异步加载
  • 过期策略
  • 基于大小
  • 基于时间
  • 基于引用
  • Caffeine.weakKeys()
  • Caffeine.weakValues()
  • Caffeine.softValues()
  • 缓存移除
  • invalidate(Object key)方法
  • invalidateAll(Iterable<?> keys)方法
  • invalidateAll()方法
  • 移除监听器(RemovalListener)
  • Writer
  • 统计
  • 参考
  • 源代码


Java高性能本地缓存框架Caffeine

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

Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。

Caffeine 是一个基于Java 8的高性能本地缓存框架,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。

文档参考:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN

如何使用

在项目中添加Caffeine的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>caffeine-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>


    <dependencies>
        <!-- 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>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

缓存加载

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

手动加载

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

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

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

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

示例:

/**
     * 手动加载cache
     */
    @Test
    public void testManualLoadCache2() throws InterruptedException {
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.SECONDS) // 设置超时时间为5s / 写入后隔段时间过期
                .maximumSize(1)// 设置缓存最大条目数,超过条目则触发回收。
                .build();
        // 查找一个缓存元素, 没有查找到的时候返回null
        String value = cache.getIfPresent("test");
        System.out.println(value);//-->null
        // 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
        value = cache.get("test", k -> "test-value");
        System.out.println(cache.getIfPresent("test"));//-->test-value
        System.out.println(value);//-->test-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);
        //查看缓存中的数据
        System.out.println(dataObjectMap.size()); //--> 1
        System.out.println(dataObjectMap); //--> {test9=test-value9}

        Thread.sleep(5000); //设置10s的睡眠时间,使得超过过期时间

        System.out.println(cache.getIfPresent("test"));//-->null
    }
  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);
                    }
                });
        System.out.println(cache.get("test1")); //第一次的时候会调用load方法
        System.out.println(cache.get("test1")); //第二次不会调用load方法
    }
手动异步加载
  • 创建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");
        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

示例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();
                            }
                            System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
                            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);
        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
    }
  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);
        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
    }
  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
         */
    }