加载方式

Cache手动加载
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
public static void main(String[] args) {
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100)
.build();

String key = "test";

// 查找一个缓存元素,没有查找到的时候返回null
String value = cache.getIfPresent(key);
System.out.println(Objects.isNull(value));

//查找缓存,如果缓存不存在则生成缓存元素,如果无法生成则返回null
value = cache.get(key, v -> createObject(key));

//添加或者更新一个缓存元素
assert value != null;
cache.put(key, value);
value = cache.getIfPresent(key);
System.out.println(value);

//移除一个缓存元素
cache.invalidate(key);
value = cache.getIfPresent(key);
System.out.println(Objects.isNull(value));
}

private static String createObject(String key) {
return key + "+value";
}

当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache. Get()可能会返回null

当然也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法直接对缓存进行操作

LoadingCache自动加载
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
37
38
39
public static void main(String[] args) {
LoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(Test02::createValue);


//查找缓存,如果缓存不存在则生成缓存元素,如果无法生成则返回null
String key = "test";
String value = cache.get(key);
System.out.println(value);

List<String> keys = new ArrayList<>();
keys.add("test01");
keys.add("test02");
//批量查找缓存,如果缓存不在则生成元素
Map<String, String> map = cache.getAll(keys);
System.out.println(map);
}

private static String createValue(String key) {
return "test+" + key;
}

public static void main(String[] args) {
AsyncCache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(100)
.buildAsync();

String key = "test";
//查找缓存元素,如果不存在,则异步生成
CompletableFuture<String> value = cache.get(key, k -> createValue(key));
System.out.println(value);
}

private static String createValue(String key) {
return "test+" + key;
}
AsyncLoadingCache自动异步加载
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.buildAsync(Test04::createValue);
}

private static String createValue(String key) {
return "test+" + key;
}

可用参数

recordStats

通过使用 Caffeine.recordStats()方法可以打开数据收集功能。Cache.stats()方法将会返回一个 CacheStats 对象,其将会含有一些统计指标,比如:

  • hitRate(): 查询缓存的命中率
  • evictionCount(): 被驱逐的缓存数量
  • averageLoadPenalty(): 新值被载入的平均耗时

这些缓存统计指标可以被基于 push/pull 模式的报告系统进行集成。基于 pull 模式的系统可以通过调用 Cache.stats() 方法获取当前缓存最新的统计快照。一个基于 push 的系统可以通过自定义一个 StatsCounter 对象达到在缓存操作发生时自动推送更新指标的目的。

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
37
38
39
40
41
42
43
44
45
//返回一个ConcurrentStatsCounter实例
static final Supplier<StatsCounter> ENABLED_STATS_COUNTER_SUPPLIER = ConcurrentStatsCounter::new;

@Nullable Supplier<StatsCounter> statsCounterSupplier;

// Enables the accumulation of {@link CacheStats} during the operation of the cache. Without this {@link Cache#stats} will return zero for all statistics. Note that recording statistics requires bookkeeping to be performed with each operation, and thus imposes a performance penalty on cache operation.
//在创建缓存的时候我们调用recordStats这个方法,创建StatsCounter实现,每次与统计相关的更改都将推送给此对象
@NonNull
public Caffeine<K, V> recordStats() {
requireState(this.statsCounterSupplier == null, "Statistics recording was already set");
statsCounterSupplier = ENABLED_STATS_COUNTER_SUPPLIER;
return this;
}

//ConcurrentStatsCounter类中的一些属性
public ConcurrentStatsCounter() {
hitCount = new LongAdder();
missCount = new LongAdder();
loadSuccessCount = new LongAdder();
loadFailureCount = new LongAdder();
totalLoadTime = new LongAdder();
evictionCount = new LongAdder();
evictionWeight = new LongAdder();
}

#示例代码

public class CaffeineTest {
public static void main(String[] args) {
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(1)
.recordStats()
.build();

cache.put("A", "a");
cache.getIfPresent("A");
cache.getIfPresent("B");
System.out.println("缓存命中次数:" + cache.stats().hitCount());
System.out.println("缓存未命中次数:" + cache.stats().missCount());
}
}

#结果
缓存命中次数:1
缓存未命中次数:1
weakKeys、weakValues 和 softValues

key支持弱引用,而value则支持弱引用和软引用,需要注意的是,AsyncCache不支持软引用和弱引用。

另外需要注意的是,使用了weakKeys后,缓存Key之间的比较将会通过引用相等(==)而不是对象相等equals()去进行。原因是GC只依赖于引用相等性。(否则的话,万一A与B是equals的,A回收但B没回收,如果是以A为key进的缓存,缓存会被回收,用户拿着B却无法再次get(B)).

同理,使用了 weakValues 或 softValues 后,value 之间的比较也会通过引用相等(==) 而不是对象相等 equals()。

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
37
38
39
40
41
//枚举,基于弱/软引用
//weakValues和softValues不可以同时使用
enum Strength { WEAK, SOFT }

//Specifies that each key (not value) stored in the cache should be wrapped in a{@link WeakReference} (by default, strong references are used).
//使用弱引用存储key,如果没有强引用这个key,则GC时允许回收该条目
@NonNull
public Caffeine<K, V> weakKeys() {
requireState(keyStrength == null, "Key strength was already set to %s", keyStrength);
requireState(writer == null, "Weak keys may not be used with CacheWriter");

keyStrength = Strength.WEAK;
return this;
}

//使用弱引用存储value,如果没有强引用这个value,则GC时允许回收该条目
@NonNull
public Caffeine<K, V> weakValues() {
requireState(valueStrength == null, "Value strength was already set to %s", valueStrength);
valueStrength = Strength.WEAK;
return this;
}

//Specifies that each value (not key) stored in the cache should be wrapped in a {@link SoftReference} (by default, strong references are used). Softly-referenced objects will be garbage-collected in a <i>globally</i> least-recently-used manner, in response to memory demand.
//使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目
@NonNull
public Caffeine<K, V> softValues() {
requireState(valueStrength == null, "Value strength was already set to %s", valueStrength);
valueStrength = Strength.SOFT;
return this;
}

//未设置参数的标志默认值,用来判断该参数有没有设置
static final int UNSET_INT = -1;

//默认初始化大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
//expireAfterWrite和expireAfterAccess的默认时间
static final int DEFAULT_EXPIRATION_NANOS = 0;
//refreshAfterWrite的默认时间
static final int DEFAULT_REFRESH_NANOS = 0;

可以使用 CaffeineSpec 提供简单的字符格式配置,不是很常用。

1
2
3
4
5
6
7
8
9
10
//严格解析标识符
boolean strictParsing = true;

//可以使用 CaffeineSpec 提供简单的字符格式配置,不是很常用。
@NonNull
public static Caffeine<Object, Object> from(CaffeineSpec spec) {
Caffeine<Object, Object> builder = spec.toBuilder();
builder.strictParsing = false;
return builder;
}
maximumSize与maximumWeight

maximumSize
缓存会通过 Window TinyLfu 算法控制整个缓存大小。关于这个算法我们在下文的原理中细讲。
maximumWeight
这种方式控制的是总权重。需要 weigher 提供为每个 entry 计算权重的方式。当我们的缓存大小不均匀时,我们可以通过这种方式控制总大小。权重计算是在其创建或更新时发生的,此后其权重值都是静态存在的。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//本地缓存的最大条数,默认为未设置参数
long maximumSize = UNSET_INT;
//本地缓存的最大权重,默认为未设置参数
long maximumWeight = UNSET_INT;
//初始的缓存空间大小,默认为未设置参数
int initialCapacity = UNSET_INT;

//指定一个权重函数
@Nullable Weigher<? super K, ? super V> weigher;

//设定最大容量,缓存会通过 Window TinyLfu 算法控制整个缓存大小
@NonNull
public Caffeine<K, V> maximumSize(@NonNegative long maximumSize) {
//maximumSize和maximumWeight不可以同时使用
requireState(this.maximumSize == UNSET_INT,
"maximum size was already set to %s", this.maximumSize);
requireState(this.maximumWeight == UNSET_INT,
"maximum weight was already set to %s", this.maximumWeight);
//maximumSize与weigher不能同时使用
requireState(this.weigher == null, "maximum size can not be combined with weigher");
requireArgument(maximumSize >= 0, "maximum size must not be negative");
this.maximumSize = maximumSize;
return this;
}

//设定最大权重,通过权重函数计算超过最大权重的缓存会被释放
@NonNull
public Caffeine<K, V> maximumWeight(@NonNegative long maximumWeight) {
requireState(this.maximumWeight == UNSET_INT,
"maximum weight was already set to %s", this.maximumWeight);
requireState(this.maximumSize == UNSET_INT,
"maximum size was already set to %s", this.maximumSize);
requireArgument(maximumWeight >= 0, "maximum weight must not be negative");
this.maximumWeight = maximumWeight;
return this;
}

//自定义权重函数
@NonNull
public <K1 extends K, V1 extends V> Caffeine<K1, V1> weigher(
@NonNull Weigher<? super K1, ? super V1> weigher) {
requireNonNull(weigher);
requireState(this.weigher == null, "weigher was already set to %s", this.weigher);
requireState(!strictParsing || this.maximumSize == UNSET_INT,
"weigher can not be combined with maximum size");

@SuppressWarnings("unchecked")
Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
self.weigher = weigher;
return self;
}

//自定义初始化容量
@NonNull
public Caffeine<K, V> initialCapacity(@NonNegative int initialCapacity) {
requireState(this.initialCapacity == UNSET_INT,
"initial capacity was already set to %s", this.initialCapacity);
requireArgument(initialCapacity >= 0);
this.initialCapacity = initialCapacity;
return this;
}
expireAfterAccess 、expireAfterWrite 与expireAfter

expireAfterAccess 表示上次读写超过一定时间后过期,expireAfterWrite 表示上次创建或更新超过一定时间后过期。expireAfter 允许复杂的表达式,过期时间可以通过 entry 等外部参数确定。

至于过期淘汰的发生,是在写操作以及偶尔发生在读操作中的。过期事件的调度和触发将会在 O(1)的时间复杂度内完成。如果希望过期发生的更及时,可以通过在你的 Cache 构造器中通过 Scheduler 接口和 Caffeine.scheduler(Scheduler) 方法去指定一个调度线程代替在缓存活动中去对过期事件进行调度。

具体地说,在默认情况下,当一个缓存元素过期的时候,Caffeine 不会自动立即将其清理和驱逐。而它将会在写操作之后进行少量的维护工作,在写操作较少的情况下,也偶尔会在读操作之后进行。如果你的缓存吞吐量较高,那么你不用去担心你的缓存的过期维护问题。但是如果你的缓存读写操作都很少,可以额外通过一个线程使用 Cache.cleanUp() 方法在合适的时候触发清理操作。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//表示上次创建或更新超过一定时间后过期,默认为未设置参数
long expireAfterWriteNanos = UNSET_INT;
//表示上次读写超过一定时间后过期,默认为未设置参数
long expireAfterAccessNanos = UNSET_INT;
//将会使在写操作之后的一段时间后允许 key 对应的缓存元素进行刷新,但是只有在这个 key 被真正查询到的时候才会正式进行刷新操作,默认为未设置参数
long refreshAfterWriteNanos = UNSET_INT;

// 基于不同的过期驱逐策略与expireAfterWriteNanos和expireAfterAccessNanos互斥
@Nullable Expiry<? super K, ? super V> expiry;

//至于过期淘汰的发生,是在写操作以及偶尔发生在读操作中的。过期事件的调度和触发将会在 O(1)的时间复杂度内完成。如果希望过期发生的更及时,可以通过在你的 Cache 构造器中通过 Scheduler 接口和 Caffeine.scheduler(Scheduler) 方法去指定一个调度线程代替在缓存活动中去对过期事件进行调度。
@NonNull
public Caffeine<K, V> expireAfterWrite(@NonNegative long duration, @NonNull TimeUnit unit) {
requireState(expireAfterWriteNanos == UNSET_INT,
"expireAfterWrite was already set to %s ns", expireAfterWriteNanos);
requireState(expiry == null, "expireAfterWrite may not be used with variable expiration");
requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterWriteNanos = unit.toNanos(duration);
return this;
}

@NonNull
public Caffeine<K, V> scheduler(@NonNull Scheduler scheduler) {
requireState(this.scheduler == null, "scheduler was already set to %s", this.scheduler);
this.scheduler = requireNonNull(scheduler);
return this;
}

@NonNull
public Caffeine<K, V> expireAfterAccess(@NonNegative long duration, @NonNull TimeUnit unit) {
requireState(expireAfterAccessNanos == UNSET_INT,
"expireAfterAccess was already set to %s ns", expireAfterAccessNanos);
requireState(expiry == null, "expireAfterAccess may not be used with variable expiration");
requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
this.expireAfterAccessNanos = unit.toNanos(duration);
return this;
}


// 基于不同的过期驱逐策略
@NonNull
public <K1 extends K, V1 extends V> Caffeine<K1, V1> expireAfter(
@NonNull Expiry<? super K1, ? super V1> expiry) {
requireNonNull(expiry);
requireState(this.expiry == null, "Expiry was already set to %s", this.expiry);
requireState(this.expireAfterAccessNanos == UNSET_INT,
"Expiry may not be used with expiresAfterAccess");
requireState(this.expireAfterWriteNanos == UNSET_INT,
"Expiry may not be used with expiresAfterWrite");

@SuppressWarnings("unchecked")
Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
self.expiry = expiry;
return self;
}
removalListener

removal(移除)包括 eviction(驱逐:由于策略自动移除)和 invalidation(失效:手动移除)

removalListener 的操作将会异步执行在一个 Executor 上。默认的线程池实现是 ForkJoinPool.commonPool()。当然也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现。这个 Executor 同时负责 refresh 等操作。

如果希望这个操作是同步的,可以通过下文的 writer() 方法实现。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//缓存删除监听器
@Nullable RemovalListener<? super K, ? super V> removalListener;

//Specifies a listener instance that caches should notify each time an entry is removed for any {@linkplain RemovalCause reason}. Each cache created by this builder will invoke this listener as part of the routine maintenance described in the class documentation above.
//创建可以监听缓存中键删除的监听器
//removalListener 的操作将会异步执行在一个 Executor 上。默认的线程池实现是 ForkJoinPool.commonPool()。当然也可以通过覆盖 Caffeine.executor(Executor) 方法自定义线程池的实现。这个 Executor 同时负责 refresh 等操作。
@NonNull
public <K1 extends K, V1 extends V> Caffeine<K1, V1> removalListener(
@NonNull RemovalListener<? super K1, ? super V1> removalListener) {
requireState(this.removalListener == null,
"removal listener was already set to %s", this.removalListener);

@SuppressWarnings("unchecked")
Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
self.removalListener = requireNonNull(removalListener);
return self;
}

@NonNull
public Caffeine<K, V> executor(@NonNull Executor executor) {
requireState(this.executor == null, "executor was already set to %s", this.executor);
this.executor = requireNonNull(executor);
return this;
}

#示例代码:
public class CaffeineTest {
public static void main(String[] args) {
Cache<Object, Object> cache = Caffeine.newBuilder()
.maximumSize(5)
.removalListener((Object key, Object value, RemovalCause cause) -> System.out.println("BasChannelLocalCache Key:" + key + " was remove case: " + cause))
.build();

List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; i++){
cache.put(i, i);
list.add(i);
}

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Map<Object, Object> map = cache.getAllPresent(list);
map.forEach((k, v) -> System.out.println("还存在的key" + k + ":" + v));
}
}

#结果
BasChannelLocalCache Key:0 was remove case: SIZE
BasChannelLocalCache Key:6 was remove case: SIZE
BasChannelLocalCache Key:5 was remove case: SIZE
BasChannelLocalCache Key:4 was remove case: SIZE
BasChannelLocalCache Key:1 was remove case: SIZE
还存在的key2:2
还存在的key3:3
还存在的key7:7
还存在的key8:8
还存在的key9:9
writer

CacheWriter 给缓存提供了充当底层资源的门面的能力,当其与 CacheLoader 一起使用的时候,所有的读和写操作都可以通过缓存向下传播。Writers 提供了原子性的操作,包括从外部资源同步的场景。这意味着在缓存中,当一个 key 的写入操作在完成之前,后续对这个 key 的其他写操作都是阻塞的,同时在这段时间内,尝试获取这个 key 对应的缓存元素的时候获取到的也将都是旧值。如果写入失败那么之前的旧值将会被保留同时异常将会被传播给调用者。

CacheWriter 将会在缓存元素被创建,更新或者移除的时候被触发。但是当一个映射被加载(比如 LoadingCache.get),重载 (比如 LoadingCache.refresh),或者生成 (比如 Map.computeIfPresent) 的时候将不会被触发。

需要注意的是,CacheWriter 将不能用在 weak keys 或者 AsyncLoadingCache 的场景。

CacheWriter 可以用来实现 write-through 和 write-back 两种模式的缓存。也可以用来整合多级缓存,或是用来作为发布同步监听器使用。

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
37
38
39
//自定义一个写入器
@Nullable CacheWriter<? super K, ? super V> writer;

//Specifies a writer instance that caches should notify each time an entry is explicitly created or modified, or removed for any reason The writer is not notified when an entry is loaded or computed. Each cache created by this builder will invoke this writer as part of the atomic operation that modifies the cache.
//指定一个写入器实例,缓存在每次显式创建、修改或删除条目时都应通知该写入器实例。当加载或计算条目时,写入器不会收到通知。这个构建器创建的每个缓存都将调用这个写入器,作为修改缓存的原子操作的一部分。
@NonNull
public <K1 extends K, V1 extends V> Caffeine<K1, V1> writer(
@NonNull CacheWriter<? super K1, ? super V1> writer) {
requireState(this.writer == null, "Writer was already set to %s", this.writer);
requireState(keyStrength == null, "Weak keys may not be used with CacheWriter");

@SuppressWarnings("unchecked")
Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
self.writer = requireNonNull(writer);
return self;
}

#示例代码:
public static void main(String[] args) {
Cache<Object, Object> graphs = Caffeine.newBuilder()
.writer(new CacheWriter<Object, Object>() {
@Override
public void write(Object key, Object graph) {
System.out.println("在写入时调用的原子操作,key: " + key + " graph: " + graph);
}
@Override
public void delete(Object key, Object graph, RemovalCause cause) {
System.out.println("在删除时调用的原子操作,key: " + key + " graph: " + graph);
}
})
.build();

graphs.put("1", "a");
graphs.invalidate("1");
}

#结果
在写入时调用的原子操作,key: 1 graph: a
在删除时调用的原子操作,key: 1 graph: a

caffeine与Guava对比

首先从结果上来看,在相同的cpu负载下,Caffeine Cache的读取和写入速度由于Guava Cache,差距在4倍以上,内存占用方面,两个没有明显的差距

初始化

Caffeine、Guava都通过builder的方式进行初始化操作,生成缓存对象,通过builder方式可以生成两种缓存对象LoadingCache(同步填充),和Cache(手动填充),LoadingCache继承Cache,相比于Cache,提供了get获取值时,如果不存在值,自动通过CacheLoader和load方法家在数据并返回的功能。

caffeine cache还提供了第三种初始化方式,异步加载的方式(见上),AsyncLoadingCache是继承自LoadingCache类的,异步加载使用Executor去调用方法并返回一个CompletableFuture,相比于同步填充模式,在load数据时,使用一步线程来执行load方法,默认使用ForkJoinPool()来执行一步线程,可以通过Caffeine.executor(Executor)方法来替换线程池。

基于时间的过期驱逐策略

guava 和caffeine都支持通过两种策略来进行数据的回收策略,分别是expireAfterWrite、expireAfterAccess,此外caffeine还支持通过expireAfter来通过重新相关方法自定义过期策略,这些过期策略都是初始化时进行指

Guava提供了两种回收策略,但是他并不是通过后台监听线程进行数据的清除工作,而是在获取值时进行值回收,所以如果一个key一直不被访问,虽然设置了过期策略,它依然会一直存在

引用

https://blog.csdn.net/zhangyunfeihhhh/article/details/108105928

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