首页 > 其他分享 >caffeine 高效缓存用法小记

caffeine 高效缓存用法小记

时间:2023-03-04 16:24:32浏览次数:56  
标签:11 load 缓存 printWithTime PrintUtils caffeine build main 小记

caffeine 高效缓存用法小记。


1. pom

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

2. demo

​ 注意这里的API基本和Guava Cache的基本一致。
只是caffiene 默认使用了ForkJoin的common 共用线程池。
​ 另外两者的Refresh 机制一样,不是我们理解的背后有定时任务去load,而是达到refresh 指定的时间后,第一次访问存在的key会把结果返回去的同时异步去刷新新的结果。
另外caffiene的缓存失效策略类似于redis,采用惰性删除,在下次操作的时候才会进行判断是否有过期key。

0. Cache 手动加载

package org.example;


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

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

public class CacheTest {

    private final static long EXPIRE = 6;

    private final static long REFRESH = 3;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Cache<Object, Object> build = Caffeine.newBuilder()
                // 设置最大缓存个数
                .maximumSize(2)
                // 过期时间。 写入后指定时间过期
                .expireAfterWrite(EXPIRE, TimeUnit.SECONDS)
                .build();

        // 查找一个缓存元素, 没有查找到的时候返回null
        Object obj1 = build.getIfPresent("1");
        PrintUtils.printWithTime(obj1 + "");
        // 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
        Object obj2 = build.get("1", new Function<Object, Object>() {

            @Override
            public Object apply(Object o) {
                PrintUtils.printWithTime(o + "-default");
                return o + "-default";
            }
        });
        PrintUtils.printWithTime("obj2: " + obj2 + "");
        // 手动添加
        build.put("1", obj2);
        PrintUtils.printWithTime("s1: " + build.getIfPresent("1") + "");
        build.invalidate("1");
        PrintUtils.printWithTime("s2: " + build.getIfPresent("1") + "");
    }
}

结果:

main	14:26:28	null
main	14:26:28	1-default
main	14:26:28	obj2: 1-default
main	14:26:28	s1: 1-default
main	14:26:28	s2: null

1. LoadingCache 自动同步加载

package org.example;


import com.github.benmanes.caffeine.cache.*;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class CacheTest {

    private final static long EXPIRE = 6;

    private final static long REFRESH = 3;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        LoadingCache<String, String> build = Caffeine.newBuilder()
                // 我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。
                .initialCapacity(2)
                // 设置最大缓存个数
                .maximumSize(2)
                // 过期时间。 写入后指定时间过期
                .expireAfterWrite(EXPIRE, TimeUnit.SECONDS)
                /**
                 * 这个参数是 LoadingCache 和 AsyncLoadingCache 的才会有的。在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。refreshAfterWrite 将会使在写操作之后的一段时间后允许 key 对应的缓存元素进行刷新,但是只有在这个 key 被真正查询到的时候才会正式进行刷新操作。
                 * 在刷新的过程中,如果抛出任何异常,会保留旧值。异常会被 logger 打印,然后被吞掉。
                 * 此外,CacheLoader 还支持通过覆盖重写 CacheLoader.reload(K, V) 方法使得在刷新中可以将旧值也参与到更新的过程中去。
                 * refresh 的操作将会异步执行在一个 Executor 上。默认的线程池实现是 ForkJoinPool.commonPool()。当然也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现。这个 Executor 同时负责 removalListener 的操作。
                 */
                .refreshAfterWrite(REFRESH, TimeUnit.SECONDS)
                .build(new CacheLoader<String, String>() {

                    private int num = 0;

                    @Override
                    public String load(String key) throws Exception {
                        PrintUtils.printWithTime("load\t" + key);
                        // 模拟获取需要1 s
                        Thread.sleep(1 * 1000);
                        String value = key + (num++);
                        PrintUtils.printWithTime("load\t" + key + "\tvalue: " + value);
//                        // 缓存加载逻辑xxxxxxxxx
                        return value;
                    }

                });

        // 第一次加载
        PrintUtils.printWithTime("start");
        String s1 = build.get("1");
        PrintUtils.printWithTime("s1:\t" + s1);
        PrintUtils.printWithTime("s10:\t" + build.get("1"));

        Thread.sleep(4 * 1000);
        String s2 = build.get("1");
        PrintUtils.printWithTime("s2:\t" + s2);

        Thread.sleep(2 * 1000);
        PrintUtils.printWithTime("s20:\t" + build.get("1"));

        Thread.sleep(20 * 1000);
    }
}

结果:可以看出本身refresh 就是异步去加载的,用的是forkjon共用线程池; 如果是查找一个不存在缓存中的元素会同步加载且等待结果。

main	10:58:37	start
main	10:58:37	load	1
main	10:58:38	load	1	value: 10
main	10:58:39	s1:	10
main	10:58:39	s10:	10
ForkJoinPool.commonPool-worker-1	10:58:43	load	1
main	10:58:43	s2:	10
ForkJoinPool.commonPool-worker-1	10:58:44	load	1	value: 11
main	10:58:45	s20:	11

2. AsyncLoadingCache 自动异步加载

package org.example;


import com.github.benmanes.caffeine.cache.*;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class CacheTest {

    private final static long EXPIRE = 6;

    private final static long REFRESH = 3;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AsyncLoadingCache<String, String> build = Caffeine.newBuilder()
                // 我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。
                .initialCapacity(2)
                // 设置最大缓存个数
                .maximumSize(2)
                // 过期时间。 写入后指定时间过期
                .expireAfterWrite(EXPIRE, TimeUnit.SECONDS)
                /**
                 * 这个参数是 LoadingCache 和 AsyncLoadingCache 的才会有的。在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。refreshAfterWrite 将会使在写操作之后的一段时间后允许 key 对应的缓存元素进行刷新,但是只有在这个 key 被真正查询到的时候才会正式进行刷新操作。
                 * 在刷新的过程中,如果抛出任何异常,会保留旧值。异常会被 logger 打印,然后被吞掉。
                 * 此外,CacheLoader 还支持通过覆盖重写 CacheLoader.reload(K, V) 方法使得在刷新中可以将旧值也参与到更新的过程中去。
                 * refresh 的操作将会异步执行在一个 Executor 上。默认的线程池实现是 ForkJoinPool.commonPool()。当然也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现。这个 Executor 同时负责 removalListener 的操作。
                 */
                .refreshAfterWrite(REFRESH, TimeUnit.SECONDS)
                .buildAsync(new CacheLoader<String, String>() {

                    private int num = 0;

                    @Override
                    public @Nullable String load(@NonNull String key) throws Exception {
                        PrintUtils.printWithTime("load\t" + key);
                        // 模拟获取需要1 s
                        Thread.sleep(1 * 1000);
                        String value = key + (num++);
                        PrintUtils.printWithTime("load\t" + key + "\tvalue: " + value);
//                        // 缓存加载逻辑xxxxxxxxx
                        return value;
                    }
                });

        // 第一次加载
        PrintUtils.printWithTime("start");
        CompletableFuture<String> stringCompletableFuture = build.get("1");
        PrintUtils.printWithTime(stringCompletableFuture.get());

        CompletableFuture<String> stringCompletableFuture2 = build.get("1");
        PrintUtils.printWithTime(stringCompletableFuture2.get());

        Thread.sleep(10 * 1000);
        CompletableFuture<String> stringCompletableFuture3 = build.get("1");
        PrintUtils.printWithTime(stringCompletableFuture3.get());
    }
}

结果:

main	14:11:18	start
ForkJoinPool.commonPool-worker-1	14:11:19	load	1
ForkJoinPool.commonPool-worker-1	14:11:20	load	1	value: 10
main	14:11:20	10
main	14:11:20	10
ForkJoinPool.commonPool-worker-2	14:11:30	load	1
ForkJoinPool.commonPool-worker-2	14:11:31	load	1	value: 11
main	14:11:31	11

3.value为null不会加入缓存

                .build(new CacheLoader<String, String>() {

                    @Override
                    public String load(String key) throws Exception {
                        PrintUtils.printWithTime("load\t" + key);
                        // 模拟获取需要1 s
                        Thread.sleep(1 * 1000);
                        String value = null;
                        if (StringUtils.equalsAny(key, "1", "2", "3")) {
                            value = key + "-default";
                        }
                        PrintUtils.printWithTime("load\t" + key + "\tvalue: " + value);
//                        // 缓存加载逻辑xxxxxxxxx
                        return value;
                    }

                });

        PrintUtils.printWithTime("start");
        PrintUtils.printWithTime(build.get("1"));
        PrintUtils.printWithTime(build.get("1"));

        PrintUtils.printWithTime(build.get("4"));
        // 可以看到返回的值如果是null 不会存入缓存
        PrintUtils.printWithTime("build.asMap().size()\t" + build.asMap().size());
        PrintUtils.printWithTime(build.get("4"));
        PrintUtils.printWithTime("build.asMap().size()\t" + build.asMap().size());

结果:

main	11:10:35	start
main	11:10:35	load	1
main	11:10:36	load	1	value: 1-default
main	11:10:36	1-default
main	11:10:36	1-default
main	11:10:36	load	4
main	11:10:37	load	4	value: null
main	11:10:37	null
main	11:10:37	build.asMap().size()	1
main	11:10:37	load	4
main	11:10:38	load	4	value: null
main	11:10:38	null
main	11:10:38	build.asMap().size()	1

4. 增加removeListener

                //  监听key 删除
                .removalListener(new RemovalListener<Object, Object>() {
                    @Override
                    public void onRemoval(@Nullable Object key, @Nullable Object value, @NonNull RemovalCause cause) {
                        PrintUtils.printWithTime("key\t" + key + "\tvalue: " + value + "\tcause: " + cause.toString());
                    }
                })
              ...
        PrintUtils.printWithTime("start");
        PrintUtils.printWithTime(build.get("1"));
        PrintUtils.printWithTime(build.get("2"));
        PrintUtils.printWithTime(build.get("3"));

        Thread.sleep(100);
        build.asMap().remove("3");

        Thread.sleep(20 * 1000);
        PrintUtils.printWithTime(build.asMap().size() + "");
        PrintUtils.printWithTime(build.get("1"));
        PrintUtils.printWithTime(build.get("4"));                  

结果:

main	11:32:11	start
main	11:32:11	load	1
main	11:32:12	load	1	value: 1-default
main	11:32:12	1-default
main	11:32:12	load	2
main	11:32:13	load	2	value: 2-default
main	11:32:13	2-default
main	11:32:13	load	3
main	11:32:14	load	3	value: 3-default
main	11:32:14	3-default
ForkJoinPool.commonPool-worker-1	11:32:14	key	2	value: 2-default	cause: SIZE
ForkJoinPool.commonPool-worker-2	11:32:14	key	3	value: 3-default	cause: EXPLICIT
main	11:32:37	1
main	11:33:09	load	1
main	11:33:10	load	1	value: 1-default
ForkJoinPool.commonPool-worker-4	11:33:10	key	1	value: 1-default	cause: EXPIRED
main	11:33:10	1-default
main	11:33:10	load	4
main	11:33:11	load	4	value: null
main	11:33:11	null

分析:可以看到是采用惰性删除,也就是说过期之后不会有定时任务去删除。而是在get的时候发起删除,查看源码com.github.benmanes.caffeine.cache.BoundedLocalCache#computeIfAbsent

  public @Nullable V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction,
      boolean recordStats, boolean recordLoad) {
    requireNonNull(key);
    requireNonNull(mappingFunction);
    long now = expirationTicker().read();

    // An optimistic fast path to avoid unnecessary locking
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
    if (node != null) {
      V value = node.getValue();
      if ((value != null) && !hasExpired(node, now)) {
        if (!isComputingAsync(node)) {
          tryExpireAfterRead(node, key, value, expiry(), now);
          setAccessTime(node, now);
        }

        afterRead(node, now, /* recordHit */ recordStats);
        return value;
      }
    }
    if (recordStats) {
      mappingFunction = statsAware(mappingFunction, recordLoad);
    }
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
    return doComputeIfAbsent(key, keyRef, mappingFunction, new long[] { now }, recordStats);
  }

6. 自定义线程池

如果不指定的话,caffeine 使用的是ForkJoin 线程池。可以自己指定线程池。

.executor(Executors.newFixedThreadPool(4))

7. 参考:

https://zhuanlan.zhihu.com/p/329684099

https://juejin.cn/post/7125674984562753572

标签:11,load,缓存,printWithTime,PrintUtils,caffeine,build,main,小记
From: https://www.cnblogs.com/qlqwjy/p/17178473.html

相关文章

  • 多项式小记
    多项式牛顿迭代对于\(G(f(x))=0\),求解\(f\pmod{x^n}\)$x^{\left\lceil\frac{n}{2}\right\rceil}$意义下的解\(f_{0}\left(x\right)\),要求模\(x^{n}\)意义下的解......
  • 缓存穿透、雪崩、击穿简单认识
    缓存穿透原因:同一时间大量请求访问缓存中不存在的数据,因为缓存中没有,所以大量请求直接进入数据库,但是数据库中也没有,导致一时间数据库请求量过大!处理办法:布隆过滤器......
  • 深入理解webpack的chunkId对线上缓存的思考(转载)
    转载自 https://juejin.cn/post/6844903924818771981#heading-6 作者:Kimm想必经常使用基于webpack打包工具的框架的同学们,无论是使用React还是Vue在性能优化上使用最......
  • 【Azure Redis 缓存】Lettuce 连接到Azure Redis服务,出现15分钟Timeout问题
    问题描述在Java应用中,使用Lettuce作为客户端SDK与AzureRedis服务连接,当遇见连接断开后,长达15分钟才会重连。导致应用在长达15分的时间,持续报错Timeout 问题解答这......
  • 归档, 反归档, 清除缓存
    1.写宏的时候可以建一个只有header的头文件2.#pragmamark如果想要实现归档和反归档的操作需要签订一个协议NSCoding-(void)encodeWithCoder:(NSCoder*)aCoder{//......
  • 分类数据展示功能_缓存优化_分析与分类数据展示功能_缓存优化_代码实现
    分类数据展示功能_缓存优化_分析1.对分类数据进行缓存优化分析发现,分类的数据在每一次页面加载后都会重新请求数据库来加载,对数据库的压力比较大,而且分类的数据不会......
  • Redis缓存雪崩
    问题描述:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB......
  • IDEA 上传项目到 Gitee 小记
    此方式可直接将IDEA中项目上传到Gitee仓库,无需打开Gitee手动创建空仓库。前提环境安装好Git,并在IDEA中成功配置;注册有Gitee账号,并记得账号密码;IDEA中安......
  • Mybatis二级缓存(2)
    10.3.一级缓存的原理          10.3.3一级缓存的测试  ......
  • Mybatis二级缓存(1)
            ......