介绍
在本文中,我们将看一看Caffeine—一个针对Java的高性能缓存库。
缓存和映射之间的一个基本区别是缓存淘汰/过期策略。
逐出策略(缓存淘汰策略)决定应在任何给定时间删除哪些对象。此策略直接影响缓存的命中率—缓存库的一个关键特性。
Caffeine使用Window TinyLfu
逐出策略,它提供了一个接近最佳的命中率。
maven依赖
我们需要把Caffeine的依赖添加到我们项目的中pom.xml
文件:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.5.5</version>
</dependency>
填充缓存
让我们重点讨论Caffeine的三种缓存填充策略:手动、同步加载和异步加载。
首先,让我们为我们将存储在缓存中的值类型编写一个类:
class DataObject {
private final String data;
private static int objectCounter = 0;
// standard constructors/getters
public static DataObject get(String data) {
objectCounter++;
return new DataObject(data);
}
}
手动填充
在这个策略中,我们手动将值放入缓存,稍后再检索它们。
让我们初始化缓存:
Cache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
现在,我们可以使用getIfPresent
方法从缓存中获取一些值。如果缓存中不存在值,则此方法将返回null:
String key = "A";
DataObject dataObject = cache.getIfPresent(key);
assertNull(dataObject);
我们可以使用put
方法手动填充缓存:
cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
assertNotNull(dataObject);
我们还可以使用get
方法获取值,该方法将函数和键作为参数。如果缓存中不存在密钥,则此函数将用于提供回退值,该值将在计算后插入缓存:
dataObject = cache
.get(key, k -> DataObject.get("Data for A"));
assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());
get
方法以原子方式执行计算。这意味着计算将只进行一次,即使多个线程同时请求值。这就是为什么使用get比使用getIfPresent更可取的原因。
有时我们需要手动使某些缓存值无效:
cache.invalidate(key);
dataObject = cache.getIfPresent(key);
assertNull(dataObject);
同步加载
这种加载缓存的方法采用一个函数,用于初始化值,类似于手动策略的get
方法。让我们看看如何使用它。
首先,我们需要初始化缓存:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
现在我们可以使用get
方法检索值:
DataObject dataObject = cache.get(key);
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
我们还可以使用getAll
方法获得一组值:
Map<String, DataObject> dataObjectMap
= cache.getAll(Arrays.asList("A", "B", "C"));
assertEquals(3, dataObjectMap.size());
从传递给生成方法的基础后端初始化函数中检索值。这使得使用缓存作为访问值的主外观成为可能。
异步加载
此策略的工作原理与前面的相同,但异步执行操作并返回一个包含实际值的CompletableFuture
:
AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.MINUTES)
.buildAsync(k -> DataObject.get("Data for " + k));
我们可以以相同的方式使用get
和getAll
方法,同时考虑到它们返回CompletableFuture
的事实:
String key = "A";
cache.get(key).thenAccept(dataObject -> {
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
});
cache.getAll(Arrays.asList("A", "B", "C"))
.thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));
淘汰value
Caffeine有三种value驱逐策略:基于大小、基于时间和基于参考。
基于大小的逐出
这种类型的逐出假定在超过缓存的配置大小限制时发生逐出。获取大小的方法有两种:计算缓存中的对象的大小,或者获取它们的权重。
让我们看看如何计算缓存中的对象。初始化缓存时,其大小等于零:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(1)
.build(k -> DataObject.get("Data for " + k));
assertEquals(0, cache.estimatedSize());
当我们添加一个值时,大小明显增加:
cache.get("A");
assertEquals(1, cache.estimatedSize());
我们可以将第二个值添加到缓存中,从而删除第一个值:
cache.get("B");
cache.cleanUp();
assertEquals(1, cache.estimatedSize());
值得一提的是,我们在获取缓存大小之前调用cleanUp
方法。这是因为缓存逐出是异步执行的,此方法有助于等待逐出的完成。
我们还可以传递一个weigher
函数来获取缓存的大小:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumWeight(10)
.weigher((k,v) -> 5)
.build(k -> DataObject.get("Data for " + k));
assertEquals(0, cache.estimatedSize());
cache.get("A");
assertEquals(1, cache.estimatedSize());
cache.get("B");
assertEquals(2, cache.estimatedSize());
当权重超过10时,这些值将从缓存中移除:
cache.get("C");
cache.cleanUp();
assertEquals(2, cache.estimatedSize());
基于时间的驱逐
此逐出策略基于条目的过期时间,有三种类型:
- Expire after access(访问后过期)-自上次读或写操作发生后经过一段时间后,条目将过期
- Expire after write(写入后过期)-自上次写入发生以来,在经过一段时间后,条目将过期
- 自定义策略-到期时间由到期实现分别计算每个条目的到期时间
让我们使用expireAfterAccess
方法配置ExpireAfter访问策略:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
要配置写后过期策略,我们使用expireAfterWrite
方法:
cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> DataObject.get("Data for " + k));
要初始化自定义策略,我们需要实现Expiry
接口:
cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() {
@Override
public long expireAfterCreate(
String key, DataObject value, long currentTime) {
return value.getData().length() * 1000;
}
@Override
public long expireAfterUpdate(
String key, DataObject value, long currentTime, long currentDuration) {
return currentDuration;
}
@Override
public long expireAfterRead(
String key, DataObject value, long currentTime, long currentDuration) {
return currentDuration;
}
}).build(k -> DataObject.get("Data for " + k));
基于引用的逐出
我们可以将缓存配置为允许对缓存键和/或值进行垃圾收集。要做到这一点,我们需要为键和值配置WeakRefence的用法,并且我们可以为值的垃圾收集配置SoftReference。
WeakRefence
用法允许在没有任何对对象的强引用时对对象进行垃圾收集。SoftReference
允许基于JVM的全局最近最少使用的策略对对象进行垃圾收集。
我们应该使用Caffeine.weakKeys()
, Caffeine.weakValues()
, 和Caffeine.softValues()
启用每个选项:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.weakKeys()
.weakValues()
.build(k -> DataObject.get("Data for " + k));
cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.softValues()
.build(k -> DataObject.get("Data for " + k));
刷新
可以将缓存配置为在定义的时间段后自动刷新条目。让我们看看如何使用refreshAfterWrite
方法执行此操作:
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
在这里,我们应该理解expireAfter
和refreshAfter
之间的区别。当请求过期的条目时,执行阻塞,直到生成函数计算出新值为止。
但如果条目符合刷新条件,则缓存将返回一个旧值并异步重新加载该值。
统计
Caffeine有一种记录缓存使用情况统计信息的方法:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build(k -> DataObject.get("Data for " + k));
cache.get("A");
cache.get("A");
assertEquals(1, cache.stats().hitCount());
assertEquals(1, cache.stats().missCount());
我们还可以传递到recordStats supplier
,后者创建StatCounter
的实现。每次与统计信息相关的更改都将推送此对象。
结论
在本文中,我们了解了Java的Caffeine缓存库。我们了解了如何配置和填充缓存,以及如何根据需要选择适当的过期或刷新策略。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1732.html
暂无评论