caffeine 高效缓存用法小记

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 end " + stringCompletableFuture);
        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());
    }
}

结果:(可以看出第一次获取元素的时候也是异步获取,返回一个CompletableFuture 对象,我们可以用该对象阻塞获取或者阻塞指定时间获取)

main	17:17:21	start
ForkJoinPool.commonPool-worker-1	17:17:21	load	1
main	17:17:21	stringCompletableFuture end java.util.concurrent.CompletableFuture@69930714[Not completed, 1 dependents]
ForkJoinPool.commonPool-worker-1	17:17:22	load	1	value: 10
main	17:17:22	10
main	17:17:22	10
ForkJoinPool.commonPool-worker-1	17:17:32	load	1
ForkJoinPool.commonPool-worker-1	17:17:33	load	1	value: 11
main	17:17:33	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. 缓存的元素在堆内存中

测试如下:

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;

public class CacheTest {

    private final static long EXPIRE = 6;

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

        Thread.sleep(20 * 1000);
        for (int i = 0; i < 10; i++) {
            Thread.sleep(20 * 1000);
            System.out.println(i + "\t ======");
            // 每次缓存100M 元素
            build.put(i, new byte[100 * 1024 * 1024]);
        }
    }

}

可以用jconsole 来查看内存变化,发现:

(1).发现堆内存每次增长基本100MB

(2).直接进入old space (有一个规则是大对象直接进入老年代)

解释:

	8bit(位)是1byte(字节)
	1024 byte(字节)是1kb
1MB 是1024KB。 也就是 byte[1024 * 1024]

8. 删除过期key

​ 默认是惰性删除,如果想自己删,可以写个定时任务自己清空。

        // 清空过期的key
        build.cleanUp();

9. 参考:

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

https://juejin.cn/post/7125674984562753572

posted @ 2023-03-04 16:07  QiaoZhi  阅读(461)  评论(0编辑  收藏  举报