Caffeine 概述

Caffeine — 一个高性能的 Java 缓存库
Caffeine 的底层数据存储采用 ConcurrentHashMap。Caffeine 面向 JDK8,在 jdk8 中 ConcurrentHashMap 增加了红黑树,在 hash 冲突严重时也能有良好的读性能。
Caffeine 是 Spring 5 默认支持的 Cache,可见 Spring 对它的看重

回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要特征。Caffeine 使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。它可以解决频率统计不准确以及访问频率衰减问题
关于淘汰策略我在淘汰策略有详细介绍可自行查阅

使用

添加依赖

1
2
3
4
5
6
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>

创建缓存

1
2
3
4
Cache<String, Object> manualCache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String... args) throws Exception {
Cache<String, String> cache = Caffeine.newBuilder()
//5秒没有读写自动删除
.expireAfterAccess(5, TimeUnit.SECONDS)
//最大容量1024个,超过会自动清理空间
.maximumSize(1024)
.removalListener(((key, value, cause) -> {
//清理通知 key,value ==> 键值对 cause ==> 清理原因
}))
.build();

//添加值
cache.put("张三", "浙江");
//获取值
cache.getIfPresent("张三");
//remove
cache.invalidate("张三");
}

key 值为空的处理方式

当访问一个不存在 key 时,我们可以返回一个默认的返回对象,来避免缓存穿透

设置假数据或显示错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String... args) throws Exception {
Cache<String, Integer> cache = Caffeine.newBuilder().build();

Integer age1 = cache.getIfPresent("张三");
System.out.println(age1);

//当key不存在时,会立即创建出对象来返回,age2不会为空
Integer age2 = cache.get("张三", k -> {
System.out.println("k:" + k);
return 18;
});
System.out.println(age2);
}

自动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String... args) throws Exception {

//此时的类型是 LoadingCache 不是 Cache
LoadingCache<String, Integer> cache = Caffeine.newBuilder().build(key -> {
System.out.println("自动填充:" + key);
return 18;
});

Integer age1 = cache.getIfPresent("张三");
System.out.println(age1);

// key 不存在时 会根据给定的CacheLoader自动装载进去
Integer age2 = cache.get("张三");
System.out.println(age2);
}

异步手动

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String... args) throws Exception {
AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();

//会返回一个 future对象, 调用future对象的get方法会一直卡住直到得到返回,和多线程的submit一样
CompletableFuture<Integer> ageFuture = cache.get("张三", name -> {
System.out.println("name:" + name);
return 18;
});

Integer age = ageFuture.get();
System.out.println("age:" + age);
}

异步自动

1
2
3
4
5
6
7
8
9
10
11
public static void main(String... args) throws Exception {
//和1.4基本差不多
AsyncLoadingCache<String, Integer> cache = Caffeine.newBuilder().buildAsync(name -> {
System.out.println("name:" + name);
return 18;
});
CompletableFuture<Integer> ageFuture = cache.get("张三");

Integer age = ageFuture.get();
System.out.println("age:" + age);
}

驱逐策略

基于大小

基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。

1
2
3
4
5
6
7
8
9
10
11
12
// Evict based on the number of entries in the cache
// 根据缓存的计数进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));

// Evict based on the number of vertices in the cache
// 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐)
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));

Caffeine.maximumSize(long):指定缓存的最大容量。当缓存超出这个容量的时候,会使用 Window TinyLfu 策略来删除缓存。

Caffeine.weigher(Weigher) 函数来指定权重,使用 Caffeine.maximumWeight(long) 函数来指定缓存最大权重值。

基于时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Evict based on a fixed expiration policy
// 基于固定的到期策略进行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));

// Evict based on a varying expiration policy
// 基于不同的到期策略进行退出
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfter(new Expiry<Key, Graph>() {
@Override
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}

@Override
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}

@Override
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));

  • expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该 key,那么这个缓存将一直不会过期。
  • expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
  • expireAfter(Expiry): 自定义策略,过期时间由 Expiry 实现独自计算。

缓存的删除策略使用的是惰性删除和定时删除。这两个删除策略的时间复杂度都是 O(1)。

基于引用

20250809184300

我们可以将缓存的驱逐配置成基于垃圾回收器。将 key 和 value 配置为弱引用或只将值配置成软引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Evict when neither the key nor value are strongly reachable
// 当key和value都没有引用时驱逐缓存
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));

// Evict when the garbage collector needs to free memory
// 当垃圾收集器需要释放内存时驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));

Caffeine.weakKeys() 使用弱引用存储 key。如果没有其他地方对该 key 有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。

Caffeine.weakValues() 使用弱引用存储 value。如果没有其他地方对该 value 有强引用,那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等,因此这会导致整个缓存使用身份 (==) 相等来比较 key,而不是使用 equals()。

Caffeine.softValues() 使用软引用存储 value。当内存满了过后,软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收,所以我们通常建议给缓存配置一个使用内存的最大值。 softValues() 将使用身份相等(identity) (==) 而不是 equals() 来比较值。

Caffeine.weakValues()和 Caffeine.softValues()不可以一起使用。

移除监听器

驱逐(eviction):由于满足了某种驱逐策略,后台自动进行的删除操作
无效(invalidation):表示由调用方手动删除缓存
移除(removal):监听驱逐或无效操作的监听器

手动删除缓存

1
2
3
4
5
6
7
// individual key
cache.invalidate(key)
// bulk keys
cache.invalidateAll(keys)
// all keys
cache.invalidateAll()

移除监听器

1
2
3
4
5
Cache<Key, Graph> graphs = Caffeine.newBuilder()
.removalListener((Key key, Graph graph, RemovalCause cause) ->
System.out.printf("Key %s was removed (%s)%n", key, cause))
.build();

RemovalListener 可以获取到 key、value 和 RemovalCause(删除的原因)

删除侦听器的里面的操作是使用 Executor 来异步执行的。默认执行程序是 ForkJoinPool.commonPool(),可以通过 Caffeine.executor(Executor)覆盖。当操作必须与删除同步执行时,请改为使用 CacheWrite

刷新

1
2
3
4
5
6
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
// 指定在创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));

Writer

1
2
3
4
5
6
7
8
9
10
11
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.writer(new CacheWriter<Key, Graph>() {
@Override public void write(Key key, Graph graph) {
// write to storage or secondary cache
}
@Override public void delete(Key key, Graph graph, RemovalCause cause) {
// delete from storage or secondary cache
}
})
.build(key -> createExpensiveGraph(key));

CacheWriter 允许缓存充当一个底层资源的代理,当与 CacheLoader 结合使用时,所有对缓存的读写操作都可以通过 Writer 进行传递。

缓存统计与监控

Caffeine 支持开启统计功能,用于监控缓存性能(命中率、加载耗时等),对调优至关重要:

1
2
3
4
5
6
7
8
9
10
LoadingCache<String, Integer> cache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats() // 开启统计
.build(key -> loadData(key));

// 获取统计信息
CacheStats stats = cache.stats();
System.out.println("命中率:" + stats.hitRate()); // 缓存命中比例
System.out.println("加载成功次数:" + stats.loadSuccessCount());
System.out.println("平均加载时间(纳秒):" + stats.averageLoadPenalty());

Cleanup

缓存的删除策略使用的是惰性删除和定时删除,但是我也可以自己调用 cache.cleanUp()方法手动触发一次回收操作。cache.cleanUp()是一个同步方法。

友链

原文链接:https://blog.csdn.net/crazymakercircle/article/details/113751575