Caffeine使用分享
缓存分类
缓存又分进程内缓存和分布式缓存两种:分布式缓存如redis、memcached等,还有本地(进程内)缓存如ehcache、GuavaCache、Caffeine等
Caffeine简介
Caffeine是一个基于Java8开发的高性能Java缓存库,可提供接近最佳的命中率,其结构和 Guava Cache 基本一样,api也一样,基本上很容易就能替换。 Caffeine 实际上就是在 Guava Cache 的基础上,利用了一些 Java 8 的新特性,提高了某些场景下的性能效率。缓存与ConcurrentMap相似,但并不完全相同。最根本的区别是ConcurrentMap会保留添加到其中的所有元素,直到将其明确删除为止。
Caffeine可以通过建造者模式灵活的组合以下特性:
- 通过异步自动加载实体到缓存中
- 基于大小的回收策略
- 基于时间的回收策略
- 自动刷新
- key自动封装虚引用
- value自动封装弱引用或软引用
- 实体过期或被删除的通知
- 写入外部资源
- 统计累计访问缓存
如何使用
在项目pom.xml中添加Caffeine的依赖
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.6</version>
</dependency>
缓存加载
Caffeine提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。
手动加载
Caffeine 有两种方式限制缓存大小。两种配置互斥,不能同时配置
- 创建一个限制容量 Cache
Cache<String, String> cache = Caffeine.newBuilder()
// 设置超时时间为5s,超过该时间缓存过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 设置缓存最大条目数,超过条目则触发回收。
.maximumSize(1)
.build();
需要注意的是,实际实现上为了性能考虑,这个限制并不会很死板:
- 在缓存元素个数快要达到最大限制的时候,过期策略就开始执行了,所以在达到最大容量前也许某些不太可能再次访问的 Entry (Key-Value)就被过期掉了。
- 有时候因为过期 Entry 任务还没执行完,更多的 Entry 被放入缓存,导致缓存的 Entry 个数短暂超过了这个限制
示例:
/**
* 手动加载cache
*/
@Test
public void testManualLoadCache2() throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
// 设置超时时间为5s,超过该时间缓存过期
.expireAfterWrite(5, TimeUnit.SECONDS)
// 设置缓存最大条目数,超过条目则触发回收。
.maximumSize(1)
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
String value = cache.getIfPresent("test");
//执行结果--> null
System.out.println(value);
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
value = cache.get("test", k -> "test-value");
//执行结果--> test-value
System.out.println(cache.getIfPresent("test"));
//执行结果--> test-value
System.out.println(value);
// 加入一些缓存数据
List<String> list = new ArrayList<>();
for (int i = 2; i < 10; i++) {
list.add("test" + i);
}
for (int i = 2; i < 10; i++) {
// 添加或者更新一个缓存元素
cache.put("test" + i, "test-value" + i);
}
// 执行缓存回收
// 缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用cache.cleanUp()方法手动触发一次回收操作。cache.cleanUp()是一个同步方法。
cache.cleanUp();
//根据key list去获取一个map的缓存
Map<String, String> dataObjectMap
= cache.getAllPresent(list);
//查看缓存中的数据
//执行结果--> 1
System.out.println(dataObjectMap.size());
//执行结果--> {test9=test-value9}
System.out.println(dataObjectMap);
//设置10s的睡眠时间,使得超过过期时间
Thread.sleep(10000);
//执行结果--> null
System.out.println(cache.getIfPresent("test"));
}
- 创建一个自定义权重限制容量的 Cache
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
//最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
.maximumWeight(1)
//每个 Entry 的 weight 值
.weigher(new Weigher<String, List<Object>>() {
@Override
public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
return value.size();
}
})
.build();
上面我们的 value 是一个 list,以 list 的大小作为 Entry 的大小。当把 Weigher 实现为只返回1,maximumWeight 其实和 maximumSize 是等效的。 同样的,为了性能考虑,这个限制也不会很死板。
示例:
@Test
public void testManualLoadCache4() {
Cache<String, List<Object>> stringListCache = Caffeine.newBuilder()
//最大weight值,当所有entry的weight和快达到这个限制的时候会发生缓存过期,剔除一些缓存
.maximumWeight(1)
//每个 Entry 的 weight 值
.weigher(new Weigher<String, List<Object>>() {
@Override
public @NonNegative int weigh(@NonNull String key, @NonNull List<Object> value) {
return value.size();
}
})
.build();
stringListCache.put("test1", Collections.singletonList("test-value1"));
stringListCache.put("test2", Arrays.asList("test-value2","test-value2"));
stringListCache.cleanUp();
Map<String, List<Object>> dataObjectMap = stringListCache.getAllPresent(Arrays.asList("test1","test2"));
System.out.println(dataObjectMap.size()); // --> 1
System.out.println(dataObjectMap); // --> {test1=[test-value1]}
}
- 指定初始大小
Cache<String, Object> cache = Caffeine.newBuilder()
//指定初始大小
.initialCapacity(1000)
.build();
和HashMap
类似,通过指定一个初始大小,减少扩容带来的性能损耗。这个值也不宜过大,浪费内存。
自动加载
- 创建LoadingCache
示例:
@DisplayName("测试LoadingCache")
@Test
public void testLoadingCache() {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, String>() {
@Override
public @Nullable String load(@NonNull String key) throws Exception {
//默认的数据加载实现,当调用get取值的时候,如果key没有对应的值,就调用这个方法进行加载
System.out.println("load data --- " + key);
//模拟从数据库中获取数据
return MAP.get(key);
}
});
//第一次的时候会调用load方法
System.out.println(cache.get("test1"));
//第二次不会调用load方法
System.out.println(cache.get("test1"));
}
手动异步加载
- 创建AsyncCache
示例:
@Test
public void testAsyncCache() throws ExecutionException, InterruptedException {
AsyncCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找缓存元素,如果不存在,则异步生成
CompletableFuture<String> value = cache.get("test1", k -> {
//异步加载
System.out.println(Thread.currentThread().getName()); // ForkJoinPool.commonPool-worker-3
System.out.println("load cache ---" + k);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return MAP.get(k);
});
System.out.println(Thread.currentThread().getName()); //main
System.out.println("=========");
System.out.println(value.get()); //value1, 阻塞
}
测试结果:
ForkJoinPool.commonPool-worker-3
load cache ---test1
main
=========
value1
自动异步加载
- 创建AsyncLoadingCache
示例1:
@Test
public void testAsynchronouslyLoadingCache() throws ExecutionException, InterruptedException {
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
//异步的封装一段同步操作来生成缓存元素
.buildAsync(new CacheLoader<String, String>() {
@Override
public @Nullable String load