缓存框架Caffeine探究

2022-01-10 15:04:07 浏览数 (1)

缓存框架Caffeine探究

  • Java高性能本地缓存框架Caffeine
  • 依赖
  • 缓存加载
    • 手动加载
    • CleanUp方法
    • 自动加载
    • 手动异步加载
    • 自动异步加载
  • 过期策略
    • 基于大小
    • 基于时间
    • 基于引用
      • Caffeine.weakKeys()
      • Caffeine.weakValues()
      • Caffeine.softValues()
  • 刷新机制
  • 缓存移除
    • invalidate(Object key)方法
    • invalidateAll(Iterable<?> keys)方法
    • invalidateAll()方法
    • 移除监听器(RemovalListener)---监听元素移除事件
    • Writer
    • 统计
  • 参考文献
  • SpringCache继承Caffeine
    • 添加依赖
    • 添加配置
    • 注解使用
  • 参考文献

Java高性能本地缓存框架Caffeine

  • 缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等
  • Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。
  • Caffeine 是一个基于Java 8的高性能本地缓存框架,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。

官方介绍Caffeine是基于JDK8的高性能本地缓存库,提供了几乎完美的命中率。它有点类似JDK中的ConcurrentMap,实际上,Caffeine中的LocalCache接口就是实现了JDK中的ConcurrentMap接口,但两者并不完全一样。最根本的区别就是,ConcurrentMap保存所有添加的元素,除非显示删除之(比如调用remove方法)。而本地缓存一般会配置自动剔除策略,为了保护应用程序,限制内存占用情况,防止内存溢出

Caffeine提供了灵活的构造方法,从而创建可以满足如下特性的本地缓存:

  • 自动把数据加载到本地缓存中,并且可以配置异步;
  • 基于数量剔除策略;
  • 基于失效时间剔除策略,这个时间是从最后一次访问或者写入算起;
  • 异步刷新;
  • Key会被包装成Weak引用;
  • Value会被包装成Weak或者Soft引用,从而能被GC掉,而不至于内存泄漏;
  • 数据剔除提醒;
  • 写入广播机制;
  • 缓存访问可以统计;

参考文档

依赖

代码语言:javascript复制
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.8.4</version>
        </dependency>

缓存加载

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

手动加载

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

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

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

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

示例:

代码语言:javascript复制
package dhy.com;

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

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class Main
{
    public static void main(String[] args) 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
    }
}
  • 创建一个自定义权重限制容量的 Cache
代码语言:javascript复制
package dhy.com;

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


import java.util.List;


public class Main
{
    public static void main(String[] args) throws InterruptedException
    {
        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 是等效的。 同样的,为了性能考虑,这个限制也不会很死板


示例:

代码语言:javascript复制
package dhy.com;

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


import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;


public class Main
{
    public static void main(String[] args) throws InterruptedException
    {
        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();

    }

    
    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]}
    }

}

  • 指定初始化大小
代码语言:javascript复制
Cache<String, Object> cache = Caffeine.newBuilder()
    //指定初始大小
    .initialCapacity(1000)
    .build();

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


CleanUp方法

默认情况下,在一个值失效之后,Caffeine没有自动或者立刻执行清理、驱逐值。相反,它在写操作之后或者读操作(写操作很少的情况)之后执行少量的维护工作。如果你的缓存是高吞吐量的,然后你不需要担心执行清理失效键值对的缓存清理工作;如果你的缓存很少读写,你可能希望有一个额外的线程去做这个清理的工作,那么可以调用Cache.cleanUp()方法。

CleanUp

一个Scheduler可能被用来开启对失效键值对的迅速删除。调度中使用批次来减少执行次数。调度会尽最大努力,但是不确保一个键值对失效时被删除。对于JAVA9 ,使用Scheduler.systemScheduler(),提供一个专门的、系统范围的调度线程。

您可以提供一个调度器用来快速删除过期条目。通过调度,会批量执行过期事件,以在短时间内最大程度的减少执行次数。调度会尽力删除过期条目,但不能对何时删除做任何保证。Java 9 的用户可能更喜欢使用Scheduler.systemScheduler()来利用专用的系统范围的调度线程。

在java9 中,可以使用Cleaner开启对基于引用键值对(weakKeys、weakValues、softValues)的迅速删除。简单使用Cleaner注册key或者value,然后调用一个Runnable,比如可以设置为Cache.cleanUp(),从而触发维护。

在Java 9 中,可以使用Cleaner来快速删除基于引用的条目(如果使用了弱引用key,弱引用value或软引用value)。只需要将key或value注册到Cleaner,其处理逻辑会调用Cache.cleanUp触发维护程序。


自动加载

  • 创建LoadingCache

示例:

代码语言:javascript复制
    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

示例:

代码语言:javascript复制
        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, 阻塞

测试结果:

代码语言:javascript复制
ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1

自动异步加载

  • 创建AsyncLoadingCache

示例1:

代码语言:javascript复制
        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 阻塞

测试结果

代码语言:javascript复制
ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1

示例2:

代码语言:javascript复制
        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);
                        });
                    }

                });

测试结果:

代码语言:javascript复制
main
load cache
ForkJoinPool.commonPool-worker-3
value1

过期策略

本地缓存的过期机制是非常重要的,因为本地缓存中的数据并不像业务数据那样需要保证不丢失。本地缓存的数据一般都会要求保证命中率的前提下,尽可能的占用更少的内存,并可在极端情况下,可以被GC掉。

Caffeine的过期机制都是在构造Cache的时候申明,主要有如下几种:

  • expireAfterWrite:表示自从最后一次写入后多久就会过期;
  • expireAfterAccess:表示自从最后一次访问(写入或者读取)后多久就会过期;
  • expireAfter:自定义过期策略

基于大小

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

示例:

代码语言:javascript复制
	@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提供了三种定时驱逐策略

  • expireAfterWrite(long, TimeUnit)

在最后一次写入缓存后开始计时,在指定的时间后过期。

示例:

代码语言:javascript复制
	@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
    }
  • expireAfterAccess(long, TimeUnit)

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

示例:

代码语言:javascript复制
        // 在最后一次读或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该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

  • expireAfter(Expiry)

在expireAfter中需要自己实现Expiry接口,这个接口支持expireAfterCreate,expireAfterUpdate,以及expireAfterRead了之后多久过期。注意这个是和expireAfterAccess、expireAfterAccess是互斥的。这里和expireAfterAccess、expireAfterAccess不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期,获取具体的过期时间。

示例:

代码语言:javascript复制
	@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之间的比较


示例:

代码语言:javascript复制
/**
     * 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之间的比较。

示例:

代码语言:javascript复制
/**
     * 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之间的比较。

示例:

代码语言:javascript复制
@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());
    }
代码语言:javascript复制
 @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则会回收掉缓存


刷新机制

在构造Cache时通过refreshAfterWrite方法指定刷新周期,例如refreshAfterWrite(10, TimeUnit.SECONDS)表示10秒钟刷新一次:

代码语言:javascript复制
.build(new CacheLoader<String, String>() {
    @Override
    public String load(String k) {
        // 这里我们就可以从数据库或者其他地方查询最新的数据
        return getValue(k);
    }
});

需要注意的是,Caffeine的刷新机制是「被动」的。举个例子,假如我们申明了10秒刷新一次。我们在时间T访问并获取到值v1,在T 5秒的时候,数据库中这个值已经更新为v2。但是在T 12秒,即已经过了10秒我们通过Caffeine从本地缓存中获取到的「还是v1」,并不是v2。在这个获取过程中,Caffeine发现时间已经过了10秒,然后会将v2加载到本地缓存中,下一次获取时才能拿到v2。即它的实现原理是在get方法中,调用afterRead的时候,调用refreshIfNeeded方法判断是否需要刷新数据。这就意味着,如果不读取本地缓存中的数据的话,无论刷新时间间隔是多少,本地缓存中的数据永远是旧的数据!


缓存移除

在构造Cache时可以通过removalListener方法申明剔除监听器,从而可以跟踪本地缓存中被剔除的数据历史信息。根据RemovalCause.java枚举值可知,剔除策略有如下5种:

  • 「EXPLICIT」:调用方法(例如:cache.invalidate(key)、cache.invalidateAll)显示剔除数据;
  • 「REPLACED」:不是真正被剔除,而是用户调用一些方法(例如:put(),putAll()等)改了之前的值;
  • 「COLLECTED」:表示缓存中的Key或者Value被垃圾回收掉了;
  • 「EXPIRED」: expireAfterWrite/expireAfterAccess约定时间内没有任何访问导致被剔除;
  • 「SIZE」:超过maximumSize限制的元素个数被剔除的原因;

invalidate(Object key)方法

示例:

代码语言:javascript复制
@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)方法

示例:

代码语言:javascript复制
	@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()方法

示例:

代码语言:javascript复制
@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)—监听元素移除事件

示例:

代码语言:javascript复制
 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]
    }



    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]
    }



    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,将对于缓存的更新,作用于其他存储,例如数据库。

示例:

代码语言:javascript复制
    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(): 新值被载入的平均耗时

示例:

代码语言:javascript复制
    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}
         */
    }


    
    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}
         */
    }


    
    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
         */
    }



    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
         */
    }



    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
         */
    }

参考文献

Java高性能本地缓存框架Caffeine

干掉GuavaCache:Caffeine才是本地缓存的王


SpringCache继承Caffeine

添加依赖

代码语言:javascript复制
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>

添加配置

注意千万不要漏了@EnableCaching

代码语言:javascript复制
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {


    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(10)
                .maximumSize(100)
                .expireAfterAccess(10, TimeUnit.MINUTES));
        cacheManager.setAllowNullValues(true);
        return cacheManager;
    }


    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(":");
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(":").append(obj);
            }
            return String.valueOf(sb);
        };
    }
}

注解使用

需要在缓存的方法上加入注解@Cacheable(cacheNames = “xxxxxx”) ,因为加上了KeyGenerator 的配置所以,key是根据KeyGenerator 来生成的,这里可以不写。

其他的注解使用请参照Spring Cache,或者可以参考我的另一篇博文关于Spring Cache Redis的集成,使用到的注解都是类似的。


参考文献

Spring Boot 中快速集成Spring Cache和Caffeine做内存缓存 玩转Spring Cache — 整合进程缓存之王Caffeine Cache和Ehcache3.x【享学Spring】 Spring Cache 集成 Caffeine实现项目缓存


0 人点赞