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();
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2021-03-04 Netty自定义任务&Future-Listener机制
2020-03-04 springboot整合shiro&shiro自定义过滤器
2019-03-04 ActiveMQ中JMS的可靠性机制