Caffeine 简单入门

作者: 右耳菌 | 来源:发表于2022-06-11 15:11 被阅读0次

    Caffeine是Google基于Java8对GuavaCache的重写升级版本,支持丰富的缓存过期策略,尤其是 TinyLfu淘汰算法,提供了一个近乎最佳的命中率。从性能上《读、写、读/写)也足以秒杀其他一堆进程内缓存框架。Spring5更是直接放弃了使用了多年的Guava,而采用了Caffeine。

    Caffeine的 API的操作功能和Guava是基本保持一致的,并且 Caffeine为了兼容之前是Guava的用户,做了一个Guava的 Adapter给大家使用也是十分的贴心。

    Caffeine是一个非常不错的缓存框架,无论是在性能方面,还是在API方面,都要比Guava cache要优秀一些。如果在新的项目中要使用local cache的话,可以优先考虑使用Caffeine。对于老的项目,如果使用了Guava cache,想要升级为 Caffeine的话,可以使用Caffeine提供的 Guava cache适配器,方便的进行切换。

    2.x 版本和3.x 版本需要的JDK版本

    根据github官网的描述,对于jdk11 以上的jdk版本请使用3.1.x,否则使用2.9.x

    For Java 11 or above, use 3.1.x otherwise use 2.9.x.


    Caffeine 的四种缓存添加策略(Java8)

    • 手动加载 Manual
    • 自动加载 Loading
    • 手动异步加载 Asynchronous (Manual)
    • 自动异步加载 Asynchronously Loading

    首先先添加maven 依赖

            <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
            <dependency>
                <groupId>com.github.ben-manes.caffeine</groupId>
                <artifactId>caffeine</artifactId>
                <version>2.9.3</version>
            </dependency>
    
    1. 手动加载
    Cache<Key, Graph> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(10_000)
        .build();
    
    // 查找一个缓存元素, 没有查找到的时候返回null
    Graph graph = cache.getIfPresent(key);
    // 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
    graph = cache.get(key, k -> createExpensiveGraph(key));
    // 添加或者更新一个缓存元素
    cache.put(key, graph);
    // 移除一个缓存元素
    cache.invalidate(key);
    
    • 这里是官方文档的描述

    Cache 接口提供了显式搜索查找、更新和移除缓存元素的能力。

    缓存元素可以通过调用 cache.put(key, value)方法被加入到缓存当中。如果缓存中指定的key已经存在对应>的缓存元素的话,那么先前的缓存的元素将会被直接覆盖掉。因此,通过 cache.get(key, k -> value) 的方>式将要缓存的元素通过原子计算的方式 插入到缓存中,以避免和其他写入进行竞争。值得注意的是,当缓存>的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null
    当然,也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

    • 一个完整的例子
    package cn.lazyfennec.caffeine;
    
    import com.github.benmanes.caffeine.cache.Cache;
    import com.github.benmanes.caffeine.cache.Caffeine;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author: Neco
     * @Description: 手动加载
     * @Date: create in 2022/6/11 11:57
     */
    public class ManualCaffeineDemo {
    
        public void test() {
            Cache<String, String> cache = Caffeine.newBuilder()
                    .expireAfterWrite(10, TimeUnit.MINUTES) // 设置过期时间
                    .maximumSize(10_000) // 设置缓存容量,其中下划线只是方便查看数字,没有实际意义,如这里就表示10000
                    .build();
    
            // 根据key查找指定的缓存
            String value = cache.getIfPresent("key");
            System.out.println("[ first time value:" + value + " ]");
            // 查询,如果没有获取到对应的数据,则执行查询获取方法,然后插入数据
            value = cache.get("key", key->getKeyValue(key));
            System.out.println("[ value of after get function return: " + value + " ]");
            value = cache.getIfPresent("key");
            System.out.println("[ value of after get function: " + value + " ]");
    
            cache.put("key", "value of after put");
            value = cache.getIfPresent("key");
            System.out.println("[ value of after put: " + value + " ]");
        }
    
        // 获取数据的方法
        private String getKeyValue(String key) {
            System.out.println("====== try to get the key's value");
            return key + "'s value";
        }
    
        public static void main(String[] args) {
            ManualCaffeineDemo caffeineDemo = new ManualCaffeineDemo();
            caffeineDemo.test();
        }
    }
    
    • 运行结果
    [ first time value:null ]
    ====== try to get the key's value
    [ value of after get function return: key's value ]
    [ value of after get function: key's value ]
    [ value of after put: value of after put ]
    

    2. 自动加载
    LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
    // Lookup and compute an entry if absent, or null if not computable
    Graph graph = cache.get(key);
    // Lookup and compute entries that are absent
    Map<Key, Graph> graphs = cache.getAll(keys);
    
    • 这里是官方文档的描述

    一个LoadingCache是一个Cache 附加上 CacheLoader能力之后的缓存实现。

    通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 CacheLoader.load 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发CacheLoader.loadAll 方法来使你的缓存更有效率。

    值得注意的是,你可以通过实现一个 CacheLoader.loadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在loadAll中也可以同时加载剩下的key对应的元素到缓存当中。

    • 一个完整的例子
    package cn.lazyfennec.caffeine;
    
    import com.github.benmanes.caffeine.cache.Caffeine;
    import com.github.benmanes.caffeine.cache.LoadingCache;
    
    import java.util.Arrays;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author: Neco
     * @Description: 自动加载
     * @Date: create in 2022/6/11 11:57
     */
    public class LoadingCaffeineDemo {
    
        public void test() {
            LoadingCache<String, String> cache = Caffeine.newBuilder()
                    .expireAfterWrite(10, TimeUnit.MINUTES) // 设置过期时间
                    .maximumSize(10_000) // 设置缓存容量,其中下划线只是方便查看数字,没有实际意义,如这里就表示10000
                    .build(key -> getKeyValue(key));
    
            // 根据key查找指定的缓存
            String value = cache.get("key");
            System.out.println(value);
    
            System.out.println("============================");
    
            Map<String, String> all = cache.getAll(Arrays.asList("key1", "key2", "key3"));
            System.out.println(all);
    
        }
    
        // 获取数据的方法
        private String getKeyValue(String key) {
            return key + "'s value";
        }
    
        public static void main(String[] args) {
            LoadingCaffeineDemo caffeineDemo = new LoadingCaffeineDemo();
            caffeineDemo.test();
        }
    }
    
    • 运行结果
    key's value
    ============================
    {key1=key1's value, key2=key2's value, key3=key3's value}
    

    3. 手动异步加载
    AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(10_000)
        .buildAsync();
    
    // 查找一个缓存元素, 没有查找到的时候返回null
    CompletableFuture<Graph> graph = cache.getIfPresent(key);
    // 查找缓存元素,如果不存在,则异步生成
    graph = cache.get(key, k -> createExpensiveGraph(key));
    // 添加或者更新一个缓存元素
    cache.put(key, graph);
    // 移除一个缓存元素
    cache.synchronous().invalidate(key);
    
    • 这里是官方文档的描述

    一个AsyncCacheCache 的一个变体,AsyncCache提供了在 Executor上生成缓存元素并返回 CompletableFuture的能力。这给出了在当前流行的响应式编程模型中利用缓存的能力。

    synchronous()方法给 Cache提供了阻塞直到异步缓存生成完毕的能力。

    当然,也可以使用 AsyncCache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。

    默认的线程池实现是 ForkJoinPool.commonPool() ,当然你也可以通过覆盖并实现 Caffeine.executor(Executor)方法来自定义你的线程池选择。

    • 一个完整的例子
    package cn.lazyfennec.caffeine;
    
    import com.github.benmanes.caffeine.cache.AsyncCache;
    import com.github.benmanes.caffeine.cache.Caffeine;
    
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Consumer;
    
    /**
     * @Author: Neco
     * @Description: 手动异步加载
     * @Date: create in 2022/6/11 11:57
     */
    public class AsynchronousManualCaffeineDemo {
    
        public void test() {
    
            String key = "key";
    
            AsyncCache<String, String> cache = Caffeine.newBuilder()
                    .expireAfterWrite(3000, TimeUnit.MILLISECONDS)
                    .maximumSize(10_000)
                    .buildAsync();
            // 查找一个缓存元素, 没有查找到的时候返回null
            CompletableFuture<String> future = cache.getIfPresent(key);
            System.out.println("first time, future: " + future);
            System.out.println("=========================");
            // 查找缓存元素,如果不存在,则异步生成
            future = cache.get(key, k -> getKeyValue(key));
            System.out.println("after try get, future = " + future);
    
            future.thenAccept(new Consumer<String>() {
                @Override
                public void accept(String s) {
                    System.out.println("accept ==== value: " + s);
                }
            });
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("future = " + future);
            System.out.println("value = " + cache.synchronous().getIfPresent(key));
            System.out.println("=====================================");
    
            // 移除一个缓存元素
            cache.synchronous().invalidate(key);
            System.out.println("future = " + future);
            System.out.println("value = " + cache.synchronous().getIfPresent(key));
            System.out.println("=====================================");
    
            // 超时自动销毁
            CompletableFuture<String> future1 = cache.get("key1", this::getKeyValue);
            try {
                Thread.sleep(1002);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(cache.synchronous().getIfPresent("key1"));
            // 继续等待超时
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(cache.synchronous().getIfPresent("key1"));
        }
    
        // 获取数据的方法
        private String getKeyValue(String key) {
            try {
                Thread.sleep(1000);
                return key + "'s value";
            } catch (Exception e) {
                e.printStackTrace();
    
            }
            return null;
        }
    
        public static void main(String[] args) {
            AsynchronousManualCaffeineDemo caffeineDemo = new AsynchronousManualCaffeineDemo();
            caffeineDemo.test();
        }
    }
    
    • 运行结果
    first time, future: null
    =========================
    after try get, future = java.util.concurrent.CompletableFuture@7ab2bfe1[Not completed, 1 dependents]
    accept ==== value: key's value
    future = java.util.concurrent.CompletableFuture@7ab2bfe1[Completed normally]
    value = key's value
    =====================================
    future = java.util.concurrent.CompletableFuture@7ab2bfe1[Completed normally]
    value = null
    =====================================
    key1's value
    null
    

    4. 自动异步加载
    AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        // 你可以选择: 去异步的封装一段同步操作来生成缓存元素
        .buildAsync(key -> createExpensiveGraph(key));
        // 你也可以选择: 构建一个异步缓存元素操作并返回一个future
        .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
    
    // 查找缓存元素,如果其不存在,将会异步进行生成
    CompletableFuture<Graph> graph = cache.get(key);
    // 批量查找缓存元素,如果其不存在,将会异步进行生成
    CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);
    
    • 这里是官方文档的描述

    一个 AsyncLoadingCache是一个 AsyncCache 加上 AsyncCacheLoader能力的实现。

    在需要同步的方式去生成缓存元素的时候,CacheLoader是合适的选择。而在异步生成缓存的场景下, AsyncCacheLoader则是更合适的选择并且它会返回一个 CompletableFuture

    通过 getAll可以达到批量查找缓存的目的。 默认情况下,在getAll 方法中,将会对每个不存在对应缓存的key调用一次 AsyncCacheLoader.asyncLoad 来生成缓存元素。 在批量检索比单个查找更有效率的场景下,你可以覆盖并开发AsyncCacheLoader.asyncLoadAll 方法来使你的缓存更有效率。

    值得注意的是,你可以通过实现一个 AsyncCacheLoader.asyncLoadAll并在其中为没有在参数中请求的key也生成对应的缓存元素。打个比方,如果对应某个key生成的缓存元素与包含这个key的一组集合剩余的key所对应的元素一致,那么在asyncLoadAll中也可以同时加载剩下的key对应的元素到缓存当中。

    • 一个完整的例子
    package cn.lazyfennec.caffeine;
    
    import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
    import com.github.benmanes.caffeine.cache.Caffeine;
    
    import java.util.Arrays;
    import java.util.Map;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ConcurrentMap;
    import java.util.concurrent.TimeUnit;
    import java.util.function.Consumer;
    
    /**
     * @Author: Neco
     * @Description: 手动同步加载
     * @Date: create in 2022/6/11 13:34
     */
    public class AsynchronouslyLoadingCaffeineDemo {
        public void test() throws Exception {
            AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
                    .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
                    .maximumSize(10_000)
                    .buildAsync(key -> getKeyValue(key));
    
            String key1 = "key1";
            //异步手动加载返回的不是值,而是 CompletableFuture
            CompletableFuture<String> future = cache.get(key1);
    
            //异步获取结果,对高性能场景有帮助
            future.thenAccept(new Consumer<Object>() {
                @Override
                public void accept(Object o) {
                    //输出结果可以看到打印时间比生成时间晚
                    System.out.println(System.currentTimeMillis() + "->" + o);
                }
            });
    
            Thread.sleep(4000);
    
            //如果 cache 中 key 为空,直接返回 null,不为空则异步取值
            CompletableFuture<String> ifPresent = cache.getIfPresent(key1);
            if (ifPresent == null) {
                System.out.println("null");
            } else {
                //异步取值
                ifPresent.thenAccept(new Consumer<Object>() {
                    @Override
                    public void accept(Object o) {
                        System.out.println(o);
                    }
                });
            }
    
            //批量异步取值,取不到则加载值
            CompletableFuture<Map<String, String>> all = cache.getAll(Arrays.asList("key1", "key2", "key3"));
    
            //批量异步获取
            all.thenAccept(new Consumer<Map<String, String>>() {
                @Override
                public void accept(Map<String, String> objectObjectMap) {
                    System.out.println(objectObjectMap);
                }
            });
    
            Thread.sleep(2001);
    
            ConcurrentMap<String, String> asMap = cache.synchronous().asMap();
            System.out.println(asMap);
        }
    
        // 获取数据的方法
        private String getKeyValue(String key) {
            return key + "_" + System.currentTimeMillis();
        }
    
        public static void main(String[] args) throws Exception {
            AsynchronouslyLoadingCaffeineDemo caffeineDemo = new AsynchronouslyLoadingCaffeineDemo();
            caffeineDemo.test();
        }
    }
    
    • 运行结果
    1654926959737->key1_1654926959730
    null
    {key1=key1_1654926963748, key2=key2_1654926963749, key3=key3_1654926963749}
    {}
    

    Caffeine 提供了三种驱逐策略

    • 基于容量 Size-based
    • 基于时间 Time-based
    • 基于引用 Reference-based
    1. 基于容量 Size-based
    // 基于缓存内的元素个数进行驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build(key -> createExpensiveGraph(key));
    
    // 基于缓存内元素权重进行驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumWeight(10_000)
        .weigher((Key key, Graph graph) -> graph.vertices().size())
        .build(key -> createExpensiveGraph(key));
    
    • 官网描述如下

    如果你的缓存容量不希望超过某个特定的大小,那么记得使用Caffeine.maximumSize(long)。缓存将会尝试通过基于就近度和频率的算法来驱逐掉不会再被使用到的元素。

    另一种情况,你的缓存可能中的元素可能存在不同的“权重”--打个比方,你的缓存中的元素可能有不同的内存占用--你也许需要借助Caffeine.weigher(Weigher) 方法来界定每个元素的权重并通过 Caffeine.maximumWeight(long)方法来界定缓存中元素的总权重来实现上述的场景。除了“最大容量”所需要的注意事项,在基于权重驱逐的策略下,一个缓存元素的权重计算是在其创建和更新时,此后其权重值都是静态存在的,在两个元素之间进行权重的比较的时候,并不会根据进行相对权重的比较。

    • 例子1 - 基于缓存内的元素个数进行驱逐
    package cn.lazyfennec.caffeine.eviction;
    
    import com.github.benmanes.caffeine.cache.Cache;
    import com.github.benmanes.caffeine.cache.Caffeine;
    import com.github.benmanes.caffeine.cache.RemovalCause;
    import com.github.benmanes.caffeine.cache.RemovalListener;
    
    /**
     * @Author: Neco
     * @Description: 基于大小-缓存大小
     * @Date: create in 2022/6/11 14:51
     */
    public class SizeBaseTest1 {
        public static void main(String[] args) {
            Cache<Object, Object> cache = Caffeine.newBuilder()
                    //缓存最大条数,超过这个条数就是驱逐缓存
                    .maximumSize(20)
                    .removalListener(new RemovalListener<Object, Object>() {
                        @Override
                        public void onRemoval(Object k, Object v, RemovalCause removalCause) {
                            System.out.println("removed " + k + " cause " + removalCause.toString());
                        }
                    })
                    .build();
    
            for (int i = 0; i < 25; i++) {
                cache.put(i, i + "_value");
            }
            cache.cleanUp();
        }
    }
    
    • 例子2 - 基于缓存内元素权重进行驱逐
    package cn.lazyfennec.caffeine.eviction;
    
    import com.github.benmanes.caffeine.cache.*;
    
    /**
     * @Author: Neco
     * @Description: 基于大小-权重
     * @Date: create in 2022/6/11 14:53
     */
    public class SizeBaseTest2 {
        public static void main(String[] args) {
            Cache<Object, Object> cache = Caffeine.newBuilder()
                    //缓存最大权重值
                    .maximumWeight(150)
                    //自定义计算权重
                    .weigher(new Weigher<Object, Object>() {
                        @Override
                        public int weigh(Object k, Object v) {
                            //这里为了简单,直接以 value 为权重
                            return (int) v;
                        }
                    })
                    .removalListener(new RemovalListener<Object, Object>() {
                        @Override
                        public void onRemoval(Object k, Object v, RemovalCause removalCause) {
                            System.out.println("removed " + k + " cause " + removalCause.toString());
                        }
                    })
                    .build();
    
            for (int i = 0; i < 20; i++) {
                cache.put(i, 10);
            }
            cache.cleanUp();
        }
    }
    

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

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

    • expireAfterAccess(long, TimeUnit): 一个元素在上一次读写操作后一段时间之后,在指定的时间后没有被再次访问将会被认定为过期项。在当被缓存的元素时被绑定在一个session上时,当session因为不活跃而使元素过期的情况下,这是理想的选择。
    • expireAfterWrite(long, TimeUnit): 一个元素将会在其创建或者最近一次被更新之后的一段时间后被认定为过期项。在对被缓存的元素的时效性存在要求的场景下,这是理想的选择。
    • expireAfter(Expiry): 一个元素将会在指定的时间后被认定为过期项。当被缓存的元素过期时间收到外部资源影响的时候,这是理想的选择。

    在写操作,和偶尔的读操作中将会进行周期性的过期事件的执行。过期事件的调度和触发将会在O(1)的时间复杂度内完成。

    为了使过期更有效率,可以通过在你的Cache构造器中通过Scheduler接口和Caffeine.scheduler(Scheduler) 方法去指定一个调度线程代替在缓存活动中去对过期事件进行调度。使用Java 9以上版本的用户可以选择Scheduler.systemScheduler()利用系统范围内的调度线程。

    当测试基于时间的驱逐策略的时候,不需要坐在板凳上等待现实时钟的转动。使用Ticker接口和 Caffeine.ticker(Ticker)方法在你的Cache构造器中去指定一个时间源可以避免苦苦等待时钟转动的麻烦。Guava的测试库也提供了FakeTicker去达到同样的目的。

    • 例子 - 基于时间进行驱逐
    package cn.lazyfennec.caffeine.eviction;
    
    import com.github.benmanes.caffeine.cache.*;
    
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Author: Neco
     * @Description: 基于时间
     * @Date: create in 2022/6/11 14:59
     */
    public class TimeBaseTest1 {
        public static void main(String[] args) throws Exception {
            LoadingCache<Object, Object> cache = Caffeine.newBuilder()
                    //基于时间失效->写入之后开始计时失效
                    .expireAfterWrite(2000, TimeUnit.MILLISECONDS)
                    //or 基于时间失效->访问之后开始计时失效
                    //.expireAfterAccess(10, TimeUnit.SECONDS)
                    //自定义线程池异步执行 remove 监听
                    .executor(Executors.newSingleThreadExecutor())
                    .removalListener(new RemovalListener<Object, Object>() {
                        @Override
                        public void onRemoval(Object k, Object v, RemovalCause removalCause) {
                            System.out.println("缓存失效了 removed " + k + " cause " + removalCause.toString());
                        }
                    })
                    //同步加载和手动加载的区别就是在构建缓存时提供一个同步的加载方法
                    .build(new CacheLoader<Object, Object>() {
                        //单个 key 的值加载
                        @Override
                        public Object load(Object key) throws Exception {
                            System.out.println("---exec load---");
                            return key + "_" + System.currentTimeMillis();
                        }
                    });
    
            //放入缓存
            cache.put("k1", "v1");
    
            //准备失效
            Thread.sleep(2001);
    
            System.out.println("sleep done");
            System.out.println("我要开始取失效的缓存了");
            Object v1 = cache.get("k1");
            System.out.println("新值 " + v1);
            System.exit(1);
        }
    }
    
    • 运行结果
    sleep done
    我要开始取失效的缓存了
    ---exec load---
    缓存失效了 removed k1 cause EXPIRED
    新值 k1_1654930796026
    

    3.基于引用
    // 当key和缓存元素都不再存在其他强引用的时候驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .weakKeys()
        .weakValues()
        .build(key -> createExpensiveGraph(key));
    
    // 当进行GC的时候进行驱逐
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .softValues()
        .build(key -> createExpensiveGraph(key));
    
    • 官网描述

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

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

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

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

    • 例子 - 基于引用驱逐
    package cn.lazyfennec.caffeine.eviction;
    
    import com.github.benmanes.caffeine.cache.Caffeine;
    import com.github.benmanes.caffeine.cache.LoadingCache;
    import com.github.benmanes.caffeine.cache.RemovalCause;
    import com.github.benmanes.caffeine.cache.RemovalListener;
    
    /**
     * @Author: Neco
     * @Description: 基于引用;Java4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
     * @Date: create in 2022/6/11 15:06
     */
    public class ReferenceBaseTest1 {
        public static void main(String[] args) throws Exception {
            // Evict when neither the key nor value are strongly reachable
            // 当key和value都没有引用时驱逐缓存
            LoadingCache<String, Object> cache1 = Caffeine.newBuilder()
                    // 使用弱引用存储key
                    .weakKeys()
                    // 使用弱引用存储value
                    .weakValues()
                    // 开启统计功能
                    .recordStats()
                    .removalListener(new RemovalListener<Object, Object>() {
                        @Override
                        public void onRemoval(Object k, Object v, RemovalCause removalCause) {
                            System.out.println("cache1 removed " + k + " cause " + removalCause.toString());
                        }
                    })
                    .build(key -> createTestValue(key));
    
            // Evict when the garbage collector needs to free memory
            // 当垃圾收集器需要释放内存时驱逐
            LoadingCache<String, Object> cache2 = Caffeine.newBuilder()
                    // 使用软引用存储value
                    .softValues()
                    // 开启统计功能
                    .recordStats()
                    .removalListener(new RemovalListener<Object, Object>() {
                        @Override
                        public void onRemoval(Object k, Object v, RemovalCause removalCause) {
                            System.out.println("cache2 removed " + k + " cause " + removalCause.toString());
                        }
                    })
                    .build(key -> createTestValue(key));
    
            Object obj1 = new Object();
            Object obj2 = new Object();
    
            cache1.put("1234", obj1);
            cache2.put("1234", obj2);
    
            obj1 = new String("123");
            obj2 = new String("123");
    
            // 主动gc
            System.gc();
    
            System.out.println(cache1.getIfPresent("1234"));
            System.out.println(cache2.getIfPresent("1234"));
    
            /*
            stats()方法会返回CacheS tats 对象以提供如下统计信息:
                hitRate():缓存命中率;
                averageLoadPenalty():加载新值的平均时间,单位为纳秒;
                evictionCount():缓存项被回收的总数,不包括显式清除。
             */
            System.out.println(cache1.stats());
            System.out.println(cache2.stats());
    
        }
    
        private static Object createTestValue(String key) {
            return key + "_" + System.currentTimeMillis();
        }
    }
    
    • 运行结果
    null
    java.lang.Object@4bf558aa
    CacheStats{hitCount=0, missCount=1, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
    CacheStats{hitCount=1, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=0, evictionWeight=0}
    

    更多内容,请查看官网文档


    各个内存缓存框架对比

    各个内存缓存框架对比

    如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~

    相关文章

      网友评论

        本文标题:Caffeine 简单入门

        本文链接:https://www.haomeiwen.com/subject/ggpbmrtx.html