1. 简介
本文将介绍 Caffeine —— 一个 高性能的 Java 缓存库。
缓存和 Map 的一个根本区别在于:缓存会自动淘汰存储的条目。
淘汰策略决定了在任意时刻应该删除哪些对象。这个策略 直接影响缓存的命中率 —— 这是缓存库的核心特性。
Caffeine 使用 Window TinyLfu 淘汰策略,提供了 接近最优的命中率。
2. 依赖配置
在 pom.xml
中添加 caffeine
依赖:
<dependency>
<groupId>com.github.ben-manes</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
最新版本可在 Maven Central 获取。
3. 缓存填充策略
Caffeine 提供 三种缓存填充策略:手动填充、同步加载和异步加载。
先定义一个缓存值类型:
class DataObject {
private final String data;
private static int objectCounter = 0;
// 标准构造器/getter
public static DataObject get(String data) {
objectCounter++;
return new DataObject(data);
}
}
3.1. 手动填充
手动存取值的策略:
初始化缓存:
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
方法获取值,接受一个 Function
参数。当键不存在时,该函数会计算值并插入缓存:
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);
3.2. 同步加载
此策略接受一个初始化函数,类似手动策略的 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());
值从传入 build
方法的 后端初始化函数 中获取。这使得缓存可作为访问值的主接口。
3.3. 异步加载
此策略 **与同步加载类似,但异步执行操作,返回包含实际值的 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()));
CompletableFuture
提供了丰富的 API,可参考 相关文章。
4. 值淘汰策略
Caffeine 提供 三种淘汰策略:基于大小、基于时间和基于引用。
4.1. 基于大小的淘汰
当 缓存超过配置的大小限制时触发淘汰。有两种计算大小的方式:对象计数或权重计算。
对象计数示例。初始化时大小为 0:
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());
4.2. 基于时间的淘汰
此策略 基于条目的过期时间,分三种类型:
- 访问后过期:条目在最后一次读/写后经过指定时间过期
- 写入后过期:条目在最后一次写入后经过指定时间过期
- 自定义策略:通过
Expiry
实现为每个条目单独计算过期时间
使用 expireAfterAccess
配置访问后过期:
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));
4.3. 基于引用的淘汰
可配置缓存 允许垃圾回收键和/或值。使用 WeakReference
回收键/值,或 SoftReference
仅回收值。
WeakReference
在对象无强引用时允许回收。SoftReference
基于 JVM 的全局 LRU 策略回收。Java 引用详情见 此处。
使用以下方法启用:
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));
5. 刷新机制
可配置缓存 在指定时间后自动刷新条目。使用 refreshAfterWrite
方法:
Caffeine.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(k -> DataObject.get("Data for " + k));
✅ 关键区别:
expireAfter
:过期条目被请求时,阻塞线程直到新值计算完成refreshAfter
:符合刷新条件的条目,返回旧值并异步加载新值
6. 统计功能
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
传入 StatsCounter
实现的工厂方法,用于自定义统计逻辑。
7. 总结
本文介绍了 Java 的 Caffeine 缓存库。我们学习了如何配置和填充缓存,以及如何根据需求选择合适的淘汰或刷新策略。
本文源码可在 GitHub 获取。