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