提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

目前处理数据经常会碰到需要将数据临时存储在缓存中,用来后续业务的处理,由于最开始使用的为ConcurrentHashMap,后续选择优化时决定使用caffeine缓存

区别

1,Caffeine 缓存可以设定删除时间等删除条件、ConcurrentMap 代表的只JAVA集合类等只能动态添加保存,除非显示的删除(有可能内存溢出)。

2, Caffeine 的读写能力显著高于ConcurrentHashMap 。

3,ConcurrentMapCacheManager 这一类基本都是基于本地内存的缓存,不支持分布式,当然caffeine也是不支持分布式的,著名的支持分布式缓存是redis,,其他的都是应用中的 基于本地应用 的缓存,即本地缓存。

简介

Caffeine 是一个 Java 缓存库,旨在提供高性能、灵活性和可定制性。它是对 Guava Cache 的改进和扩展,旨在解决 Guava Cache 在高并发和大规模数据集情况下的一些性能和扩展性问题。

以下是 Caffeine 的一些主要特点和优势:

高性能:Caffeine 的设计目标之一是提供卓越的性能。它通过使用特定的数据结构和算法,以及针对多核处理器优化的并发策略,实现了高并发环境下的低延迟和高吞吐量。

低内存消耗:Caffeine 设计时考虑了内存效率,尽量减少内存占用。它采用了一些内存优化技术,如对象池、引用队列等,以减少对象的创建和销毁,从而降低内存消耗。

异步加载:Caffeine 支持异步加载缓存项的功能,通过使用 AsyncLoadingCache 接口和相关的异步加载器,可以在后台线程中异步加载缓存项,避免阻塞主线程。

灵活性和可定制性:Caffeine 提供了丰富的配置选项和定制功能,允许开发人员根据具体需求调整缓存的行为和性能特性。可以通过设置不同的参数来调整缓存的大小、过期策略、驱逐策略等。

支持缓存统计和监控:Caffeine 提供了丰富的缓存统计和监控功能,可以实时监控缓存的命中率、加载时间、大小等指标,帮助开发人员了解缓存的使用情况并进行优化。

线程安全:Caffeine 是线程安全的,可以安全地在多个线程中并发使用,无需额外的同步措施。

实际上Caffeine这样的本地缓存和ConcurrentMap很像,即支持并发,并且支持O(1)时间复杂度的数据存取。二者的主要区别在于:
ConcurrentMap将存储所有存入的数据,直到你显式将其移除;
Caffeine将通过给定的配置,自动移除“不常用”的数据,以保持内存的合理占用。
因此,一种更好的理解方式是:Cache是一种带有存储和移除策略的Map。
缓存组件

特点

1.自动加载条目到缓存中,可选异步方式
2.可以基于大小剔除
3.可以设置过期时间,时间可以从上次访问或上次写入开始计算
4.异步刷新
5.keys自动包装在弱引用中
6.values自动包装在弱引用或软引用中
7.条目剔除通知
8.缓存访问统计

正文

1.引入jar包

 		<dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.3</version>
        </dependency>

2.手动加载

// 构建caffeine的缓存对象,并指定在写入后的10分钟内有效,且最大允许写入的条目数为10000
    private static final Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10000)
                .build();
    public static void main(String[] args) {
      /*  Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10000)
                .build();*/
        String key = "hello";
        // 查找某个缓存元素,若找不到则返回null
        String str = cache.getIfPresent(key);
        System.out.println("cache.getIfPresent(key) ---> " + str);
        // 查找某个缓存元素,若找不到则调用函数生成,如无法生成则返回null
        str = cache.get(key,k->key);
        System.out.println("cache.get(key, k -> create(key)) ---> " + str);
        // 添加或者更新一个缓存元素
        cache.put(key, "aaaa");
        System.out.println("cache.put(key, str) ---> " + cache.getIfPresent(key));
        // 移除一个缓存元素
        cache.invalidate(key);
        System.out.println("cache.invalidate(key) ---> " + cache.getIfPresent(key));
    }

Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力。
推荐使用cache.get(key, k -> value)操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。一次 cache.put(key, value) 操作将会直接写入或者更新缓存里的缓存元素,在缓存中已经存在的该key对应缓存值都会直接被覆盖。也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作

3.自动加载

 private static final LoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(LoadingDemo::create);// 当调用get或者getAll时,若找不到缓存元素,则会统一调用create(key)生成

    public static void main(String[] args) {
        String key = "hello";
        String str = cache.get(key);
        System.out.println("cache.get(key) ---> " + str);
        List<String> keys = ListUtil.toList("a", "b", "c", "d", "e");
        // 批量查找缓存元素,如果缓存不存在则生成缓存元素
        Map<String, String> maps = cache.getAll(keys);
        System.out.println("cache.getAll(keys) ---> " + maps);
    }

    private static String create(Object key) {
        // 可在此处做未获取到数据后的逻辑处理
        // 如获取不到进入redis进行查询
        return key + "world";
    }

在Caffeine中,CacheLoader 也是一个关键组件,它负责定义缓存项的加载逻辑,类似于Google Guava中的CacheLoader。下面是关于Caffeine中CacheLoader的介绍:

缓存项加载:

CacheLoader 中最重要的方法是 V load(K key),它接收一个键 key,并返回相应的值 V。开发人员需要实现这个方法,定义如何根据键加载对应的值。
自定义加载逻辑:

通过继承 CacheLoader 类并实现 load(K key) 方法,开发人员可以自定义缓存项的加载逻辑。这意味着可以在加载时进行复杂的计算、数据读取、网络请求等操作。
自动加载:

当使用 Cache(Caffeine缓存)时,在获取缓存项时如果缓存中不存在对应的值,Cache 就会使用关联的 CacheLoader 来自动加载该值。这样可以简化代码,避免了手动检查缓存中是否有值的逻辑。
原子性加载:

CacheLoader 提供了对缓存加载操作的原子性保证。即使在高并发环境下,同一个键的加载操作也只会被执行一次,避免了重复加载和线程安全问题。
异常处理:

在 load(K key) 方法中,开发人员可以处理可能出现的异常情况。如果在加载过程中发生了异常,Cache 会将异常包装在 ExecutionException 中并抛出,开发人员可以在调用 get 方法时捕获并处理。
通过使用CacheLoader,Caffeine提供了一种灵活而强大的方式来定义和控制缓存项的加载过程,使得开发人员能够根据具体需求定制缓存的加载逻辑,以满足各种复杂的应用场景。

4.手动异步加载

 static CacheLoader<String, String> loader = new CacheLoader<String,String>() {


        @Override
        public String load(String key) throws Exception {
            // 模拟数据加载过程
            Thread.sleep(1000);
            return key.toString().toUpperCase();
        }

        @Override
        public CompletableFuture<String> asyncLoad(String key, Executor executor) {
            return CompletableFuture.supplyAsync(() -> {
                try {
                    return load(key);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }, executor);
        }
    };
    private final static AsyncCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .executor(Executors.newSingleThreadExecutor())
            .buildAsync(loader);
    public void test() throws ExecutionException, InterruptedException {
        // 异步获取
        CompletableFuture<String> future = cache.get("dddd",e->"AAA");
        CompletableFuture<String> ss = cache.get("ss",e->"ss");
        CompletableFuture<String> ssss = cache.get("ssss",e->"ssss");
        CompletableFuture<String> sss = cache.get("sss",e->"sss");
        System.out.println(future.get());  // 输出 HELLO
        System.out.println(ss.get());  // 输出 HELLO
        System.out.println(ssss.get());  // 输出 HELLO
        System.out.println(sss.get());  // 输出 HELLO
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new AsynchronousDemo().test();
    }

AsyncCache 是 Caffeine 缓存库中的一个接口,用于表示带有异步加载功能的缓存。它扩展了 Cache 接口,并添加了支持异步加载缓存项的方法。以下是关于 AsyncCache 的介绍:

异步加载:

AsyncCache 允许开发人员定义一个异步加载器(AsyncCacheLoader),用于在缓存中不存在某个键对应的值时异步加载该值。这样,在调用 get(K key) 方法获取某个键对应的值时,如果该键不存在于缓存中,AsyncCache 会自动使用异步加载器在后台线程中加载该值,并将其存入缓存。
原子性加载:

与 LoadingCache 类似,AsyncCache 提供的加载方法也是原子性的。即使在高并发环境下,同一个键的加载操作也只会被执行一次。这种保证确保了加载过程中不会出现重复加载的情况,从而提高了缓存的性能和可靠性。
异常处理:

在异步加载过程中,如果加载器抛出异常,则会将异常包装在 CompletionException 中并抛出。开发人员可以通过捕获该异常来处理加载过程中可能出现的异常情况。
异步加载器:

异步加载器(AsyncCacheLoader)是实现异步加载逻辑的关键组件。开发人员需要实现 CompletableFuture asyncLoad(K key, Executor executor) 方法,定义如何在后台线程中加载缓存项的值。这使得可以在加载时执行耗时的操作,如数据库查询、远程调用等。
手动刷新:

与 LoadingCache 不同,AsyncCache 不提供自动刷新机制。但是,开发人员可以通过调用 refresh(K key) 方法手动触发对指定键对应缓存项的刷新操作,重新加载缓存项并更新缓存中的值。
通过 AsyncCache 接口,Caffeine 提供了一种方便而强大的异步加载缓存解决方案,可以在需要缓存数据的同时异步加载并保证线程安全性。

5.自动异步加载


    private final static AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            // 也可以使用下面的方式来异步构建缓存,并返回一个future
            // .buildAsync((key, executor) -> createAsync(key, executor));
            .buildAsync(key -> key);

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        String key = "Hello";
        // 查找某个缓存元素,若找不到则会异步生成。
        CompletableFuture<String> value = cache.get(key);
        System.out.println("cache.get(key, k -> create(key)) ---> " + value.get().toLowerCase());
        List<String> keys = ListUtil.toList("a", "b", "c", "d", "e");
        // 批量查找某些缓存元素,若找不到则会异步生成。
        CompletableFuture<Map<String, String>> graphs = cache.getAll(keys);
        System.out.println("cache.get(key, k -> create(key)) ---> " + graphs.get());

    }

AsyncLoadingCache 是 Caffeine 缓存库中的一个接口,它是 AsyncCache 的一个子接口。AsyncLoadingCache 不仅支持异步加载缓存项,还提供了更为方便的异步操作方法。以下是关于 AsyncLoadingCache 的详细介绍:

主要特点:
异步加载:

AsyncLoadingCache 允许开发人员定义一个异步加载器(AsyncCacheLoader),用于在缓存中不存在某个键对应的值时异步加载该值。这样,在调用 get(K key) 方法获取某个键对应的值时,如果该键不存在于缓存中,AsyncLoadingCache 会自动使用异步加载器在后台线程中加载该值,并将其存入缓存。
原子性加载:

与 LoadingCache 和 AsyncCache 类似,AsyncLoadingCache 提供的加载方法也是原子性的。即使在高并发环境下,同一个键的加载操作也只会被执行一次。这种保证确保了加载过程中不会出现重复加载的情况,从而提高了缓存的性能和可靠性。
异常处理:

在异步加载过程中,如果加载器抛出异常,则会将异常包装在 CompletionException 中并抛出。开发人员可以通过捕获该异常来处理加载过程中可能出现的异常情况。
异步加载器:

异步加载器(AsyncCacheLoader)是实现异步加载逻辑的关键组件。开发人员需要实现 CompletableFuture asyncLoad(K key, Executor executor) 方法,定义如何在后台线程中加载缓存项的值。这使得可以在加载时执行耗时的操作,如数据库查询、远程调用等。
手动刷新:

与 LoadingCache 不同,AsyncLoadingCache 提供了自动刷新机制。开发人员可以通过调用 refresh(K key) 方法手动触发对指定键对应缓存项的刷新操作,重新加载缓存项并更新缓存中的值。
异步方法:

AsyncLoadingCache 提供了一系列异步方法,如 getAsync(K key)、getAsync(K key, Executor executor) 等,允许开发人员以异步方式获取缓存项,这些方法返回的是 CompletableFuture,使得可以更加灵活地在异步编程模式下使用缓存。

策略

时间策略

  public static void main(String[] args) {
        // 自上一次写入或者读取缓存开始,在经过指定时间之后过期。
        LoadingCache<String, String> fixedAccess = Caffeine.newBuilder()
                .expireAfterAccess(5, TimeUnit.MINUTES)
                .build(key -> key);
        // 自缓存生成后,经过指定时间或者一次替换值之后过期。
        LoadingCache<String, String> fixedWrite = Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build(key -> key);
        // 自定义缓存过期策略,可以在创建时,写入后、读取时。
        LoadingCache<String, String> varying = Caffeine.newBuilder()
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(String key, String value, long currentTime) {
                        return currentTime;
                    }
                    @Override
                    public long expireAfterUpdate(String key, String value,
                                                  long currentTime, long currentDuration) {
                        return currentDuration;
                    }
                    @Override
                    public long expireAfterRead(String key, String value,
                                                long currentTime, long currentDuration) {
                        return currentDuration;
                    }
                })
                .build(key -> key);
    }

基于容量的驱逐策略

 public static void main(String[] args) {
        // 基于缓存内元素的个数,尝试回收最近或者未经常使用的元素
        LoadingCache<String, String> values = Caffeine.newBuilder().maximumSize(10_000).build(key -> key);
        // 也可以基于缓存元素的权重,进行驱除
        LoadingCache<String, String> graphs = Caffeine.newBuilder().maximumWeight(10_000).weigher((String key, String value) -> value.length()).build(key -> key);

        // 删除
        // 直接删除
        values.invalidate("key");
        // 批量删除
        values.invalidateAll(ListUtil.toList("a", "b", "c", "d", "e"));
        // 删除所有
        values.invalidateAll();

    }

总结

以上展示了单独使用的四种方式,以及所创建时设置的驱逐策略

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐