Caffeine Expiry
Expiry<K, V>
接口
Expiry
是 Caffeine 中一个非常强大且灵活的功能,它允许用户为缓存中的每个条目(Entry)动态地、精细地控制其过期时间。这与 expireAfterAccess(Duration)
或 expireAfterWrite(Duration)
这种对整个缓存应用统一固定过期策略的方式形成了鲜明对比。
Expiry
接口的核心语义是:根据条目的具体情况(Key、Value)和发生的事件(创建、更新、读取),来计算出该条目下一次应该在多长时间后过期。
这个接口定义了三个核心方法,分别对应缓存条目生命周期中的三个关键事件:
// ... existing code ...
@NullMarked
public interface Expiry<K, V> {/*** ...* @return the length of time before the entry expires, in nanoseconds*/long expireAfterCreate(K key, V value, long currentTime);/*** ...* @return the length of time before the entry expires, in nanoseconds*/long expireAfterUpdate(K key, V value, long currentTime, long currentDuration);/*** ...* @return the length of time before the entry expires, in nanoseconds*/long expireAfterRead(K key, V value, long currentTime, long currentDuration);
// ... existing code ...
expireAfterCreate(K key, V value, long currentTime)
- 触发时机: 当一个新条目被创建并插入缓存时(例如,通过
put
一个新键,或者build(loader)
加载一个新值)。 - 作用: 计算这个新条目的初始存活时间(duration)。返回值是一个以纳秒为单位的时长。你可以根据
key
和value
的内容来决定这个时长。例如,一个重要的value
可以给更长的存活时间。
- 触发时机: 当一个新条目被创建并插入缓存时(例如,通过
expireAfterUpdate(K key, V value, long currentTime, long currentDuration)
- 触发时机: 当一个已存在的条目的值被更新时(例如,通过
put
一个已存在的键)。 - 作用: 计算更新后的条目的新存活时间。它接收一个
currentDuration
参数,表示该条目当前的剩余存活时间。你可以选择返回一个新的时长来重置过期时间,也可以直接返回currentDuration
来保持原有的过期时间不变。
- 触发时机: 当一个已存在的条目的值被更新时(例如,通过
expireAfterRead(K key, V value, long currentTime, long currentDuration)
- 触发时机: 当一个已存在的条目被读取时(例如,通过
get
方法)。 - 作用: 计算读取后的条目的新存活时间。这允许你实现“访问后延长生命周期”的逻辑。同样,你可以返回一个新的时长,或者返回
currentDuration
来保持过期时间不变。
- 触发时机: 当一个已存在的条目被读取时(例如,通过
一个关键点:Caffeine 只为每个条目保留一个过期时间点。这三个方法返回的都是一个时长(duration),Caffeine 内部会用 currentTime + duration
来计算出未来的一个绝对过期时间戳。后续的更新或读取操作可以延长或缩短这个过期时间。
静态工厂方法与内部实现类
为了方便用户使用,Expiry
接口提供了三个静态工厂方法,它们分别对应了最常见的三种过期策略,并返回了预置的实现类实例。
static <K, V> Expiry<K, V> creating(BiFunction<K, V, Duration> function)
- 作用: 创建一个只在创建时计算过期时间的
Expiry
实现。后续的更新和读取操作不会改变过期时间。 - 实现: 返回
ExpiryAfterCreate<K, V>
类的实例。这个类的expireAfterUpdate
和expireAfterRead
方法直接返回currentDuration
,表示不作任何修改。
// ... existing code ... final class ExpiryAfterCreate<K, V> implements Expiry<K, V>, Serializable { // ... existing code ...@Override public long expireAfterCreate(K key, V value, long currentTime) {return toNanosSaturated(function.apply(key, value));}@CanIgnoreReturnValue@Override public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) {return currentDuration; // 不改变过期时间}@CanIgnoreReturnValue@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {return currentDuration; // 不改变过期时间} } // ... existing code ...
- 作用: 创建一个只在创建时计算过期时间的
static <K, V> Expiry<K, V> writing(BiFunction<K, V, Duration> function)
- 作用: 创建一个在创建或更新时都会重新计算过期时间的
Expiry
实现。读取操作不影响过期。 - 实现: 返回
ExpiryAfterWrite<K, V>
类的实例。它的expireAfterRead
方法返回currentDuration
。
// ... existing code ... final class ExpiryAfterWrite<K, V> implements Expiry<K, V>, Serializable { // ... existing code ...@Override public long expireAfterCreate(K key, V value, long currentTime) {return toNanosSaturated(function.apply(key, value));}@Override public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) {return toNanosSaturated(function.apply(key, value)); // 重新计算}@CanIgnoreReturnValue@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {return currentDuration; // 不改变} } // ... existing code ...
- 作用: 创建一个在创建或更新时都会重新计算过期时间的
static <K, V> Expiry<K, V> accessing(BiFunction<K, V, Duration> function)
- 作用: 创建一个在创建、更新或读取时都会重新计算过期时间的
Expiry
实现。 - 实现: 返回
ExpiryAfterAccess<K, V>
类的实例。它的三个方法都会调用用户提供的function
来计算新的过期时长。
// ... existing code ... final class ExpiryAfterAccess<K, V> implements Expiry<K, V>, Serializable { // ... existing code ...@Override public long expireAfterCreate(K key, V value, long currentTime) {return toNanosSaturated(function.apply(key, value));}@Override public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) {return toNanosSaturated(function.apply(key, value));}@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {return toNanosSaturated(function.apply(key, value));} }
- 作用: 创建一个在创建、更新或读取时都会重新计算过期时间的
使用场景示例
Expiry
接口的强大之处在于其灵活性。例如:
- 基于价值的缓存: 对于一个缓存证券价格的应用,可以为热门股票设置较短的过期时间(如5分钟),而为冷门股票设置较长的过期时间(如1小时)。
- 防止缓存雪崩: 在
expireAfterCreate
中,可以给过期时间增加一个小的随机值,避免大量缓存在同一时刻集体失效,从而分散后端服务的压力。 - 动态调整: 在
expireAfterUpdate
中,可以根据新value
的某些属性(如大小、重要性)来决定是否要延长或缩短其在缓存中的生命周期。
总结
public interface Expiry<K, V>
是 Caffeine 提供的一个高级定制化功能,它将缓存条目的过期决策权完全交给了开发者。
- 核心语义: 它定义了在创建、更新、读取三个事件点上,如何根据条目的
key
和value
计算其存活时长。 - 灵活性: 允许为每个条目设置不同的、动态变化的过期策略,而不是整个缓存一刀切。
- 易用性: 通过
creating
,writing
,accessing
三个静态工厂方法和对应的内部实现类,为最常见的使用模式提供了便捷的创建方式。
AsyncExpiry
AsyncExpiry
是 Async
工具类中的一个内部类,它的作用是将一个普通的 Expiry<K, V>
策略适配到异步缓存 AsyncCache<K, V>
的场景中。
在 AsyncCache
中,缓存的值(Value)不再是普通的对象 V
,而是一个 CompletableFuture<V>
。这就带来了一个核心问题:当一个 CompletableFuture
还没有计算完成时,它的过期策略应该如何处理? AsyncExpiry
就是为了解决这个问题而设计的。
AsyncExpiry
的核心思想是,它将一个缓存条目的生命周期看作两种状态:
- 加载中(Pending):
CompletableFuture
还没有完成(isDone()
为false
)。 - 已就绪(Ready):
CompletableFuture
已经成功完成,并且持有一个非null
的值。
针对这两种状态,AsyncExpiry
采取了截然不同的过期策略。
// ... existing code ...static final class AsyncExpiry<K, V> implements Expiry<K, CompletableFuture<V>>, Serializable {private static final long serialVersionUID = 1L;// 持有一个用户真正定义的、针对普通值 V 的过期策略final Expiry<? super K, ? super V> delegate;AsyncExpiry(Expiry<? super K, ? super V> delegate) {this.delegate = requireNonNull(delegate);}
// ... existing code ...
- 实现接口: 它实现了
Expiry<K, CompletableFuture<V>>
接口,这表明它是一个用于处理CompletableFuture
值的过期策略。 - 委托模式 (Delegate Pattern): 它内部持有一个
delegate
字段,这个delegate
就是用户通过Caffeine.newBuilder().expireAfter(userExpiry)
配置的那个普通的Expiry<K, V>
实例。AsyncExpiry
的工作就是将调用“翻译”并委托给这个delegate
。
我们来看它的三个核心方法是如何实现这种双重状态逻辑的。
expireAfterCreate
// ... existing code ...@Overridepublic long expireAfterCreate(K key, CompletableFuture<V> future, long currentTime) {if (isReady(future)) {// 状态:已就绪// 1. 从 future 中获取真实的值 V// 2. 调用用户定义的 delegate 策略来计算过期时长long duration = delegate.expireAfterCreate(key, future.join(), currentTime);// 3. 确保时长不超过Caffeine内部的最大值return Math.min(duration, MAXIMUM_EXPIRY);}// 状态:加载中// 返回一个非常大的特殊值,相当于“永不”过期return ASYNC_EXPIRY;}
// ... existing code ...
- 当 Future 已就绪 (
isReady(future)
为true
):- 它会从
future
中join()
出最终的计算结果V
。 - 然后调用被委托的
delegate.expireAfterCreate()
方法,传入真实的key
和value
,让用户定义的策略来计算过期时间。 - 最后,确保返回的时长不会超过一个内部设定的最大值
MAXIMUM_EXPIRY
。
- 它会从
- 当 Future 仍在加载中 (
isReady(future)
为false
):- 它直接返回一个静态常量
ASYNC_EXPIRY
。这个常量是一个非常大的值(大约220年),实际上就是告诉 Caffeine:“这个条目现在还不应该被过期策略所驱逐”。这是非常关键的设计,它保护了正在计算中的任务,避免因为耗时较长而被错误地清理掉。
- 它直接返回一个静态常量
expireAfterUpdate
// ... existing code ...@Overridepublic long expireAfterUpdate(K key, CompletableFuture<V> future,long currentTime, long currentDuration) {if (isReady(future)) {// 状态:已就绪// 判断当前时长是否是 ASYNC_EXPIRY,如果是,说明是从“加载中”状态刚变为“已就绪”long duration = (currentDuration > MAXIMUM_EXPIRY)// 刚完成加载,行为等同于 Create? delegate.expireAfterCreate(key, future.join(), currentTime)// 已经是就绪状态的更新,行为是 Update: delegate.expireAfterUpdate(key, future.join(), currentTime, currentDuration);return Math.min(duration, MAXIMUM_EXPIRY);}// 状态:加载中return ASYNC_EXPIRY;}
// ... existing code ...
这里的逻辑稍微复杂一点:
- 当 Future 已就绪:
- 它会检查
currentDuration
。如果这个值大于MAXIMUM_EXPIRY
(这暗示了它之前的值就是ASYNC_EXPIRY
),说明这个条目是刚刚从“加载中”状态转换到“已就绪”状态。在这种情况下,它的行为应该和“创建”时一样,所以调用delegate.expireAfterCreate()
。 - 如果
currentDuration
是一个正常值,说明这是一个对已完成条目的普通更新,于是调用delegate.expireAfterUpdate()
。
- 它会检查
- 当 Future 仍在加载中: 同样返回
ASYNC_EXPIRY
,保护其不被驱逐。
expireAfterRead
// ... existing code ...@Overridepublic long expireAfterRead(K key, CompletableFuture<V> future,long currentTime, long currentDuration) {if (isReady(future)) {// 状态:已就绪// 直接委托给用户的 read 策略long duration = delegate.expireAfterRead(key, future.join(), currentTime, currentDuration);return Math.min(duration, MAXIMUM_EXPIRY);}// 状态:加载中return ASYNC_EXPIRY;}
// ... existing code ...
这个方法的逻辑最简单:
- 当 Future 已就绪: 直接调用
delegate.expireAfterRead()
。 - 当 Future 仍在加载中: 返回
ASYNC_EXPIRY
。
总结
AsyncExpiry
是一个精巧的适配器(Adapter),它优雅地解决了异步计算场景下的过期问题。
- 核心职责: 桥接同步的
Expiry<K, V>
策略和异步缓存中CompletableFuture<V>
的值。 - 关键机制: 通过检查
CompletableFuture
的完成状态 (isReady
),将条目分为“加载中”和“已就绪”两种模式。- 对于加载中的条目,它返回一个超长的过期时间
ASYNC_EXPIRY
,有效地 “暂停”了过期驱逐 ,保护了正在进行的计算任务。 - 对于已就绪的条目,它才从
Future
中取出真实的值,并将计算委托给用户定义的原始Expiry
策略。
- 对于加载中的条目,它返回一个超长的过期时间
- 状态转换: 在
expireAfterUpdate
中,它能智能地识别出从“加载中”到“已就绪”的状态转换,并调用正确的create
逻辑来设置初始的过期时间。
没有 AsyncExpiry
,Caffeine 的异步缓存将无法正确处理带有过期时间的条目,因为它不知道如何处理一个尚未完成的 CompletableFuture
。这个类是 AsyncCache
功能完整性的重要一环。
FixedExpireAfterWrite
BoundedLocalCache
中的内部类FixedExpireAfterWrite<K, V>
这是一个非常有趣且高度特化的类。虽然它的名字里有 ExpireAfterWrite
,但它并不是我们在 Caffeine.newBuilder().expireAfterWrite(...)
中使用的那个全局的过期策略实现。它是一个内部辅助类,主要用于 VarExpiration
(可变过期策略)的特定场景。
// ... existing code ...static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {final long duration;final TimeUnit unit;FixedExpireAfterWrite(long duration, TimeUnit unit) {this.duration = duration;this.unit = unit;}@Override public long expireAfterCreate(K key, V value, long currentTime) {return unit.toNanos(duration);}@Override public long expireAfterUpdate(K key, V value, long currentTime, long currentDuration) {return unit.toNanos(duration);}@CanIgnoreReturnValue@Override public long expireAfterRead(K key, V value, long currentTime, long currentDuration) {return currentDuration;}}
// ... existing code ...
- 字段: 它有两个字段,
duration
和unit
,用于存储一个固定的时长。 - 核心作用: 它的作用是创建一个临时的、一次性的
Expiry
策略对象。这个策略的行为是:- 在创建(
expireAfterCreate
)或更新(expireAfterUpdate
)条目时,返回一个固定的、在构造时指定的过期时长(unit.toNanos(duration)
)。 - 在读取(
expireAfterRead
)条目时,不改变当前的过期时间(return currentDuration
)。
- 在创建(
这个行为模式和我们之前分析过的 Expiry.writing(...)
返回的 ExpiryAfterWrite
类非常相似。但关键在于它的使用场景。
- expireAfterCreate 和 expireAfterUpdate 都返回固定的 duration,因为创建和更新都是“写入”行为。FixedExpireAfterWrite 的核心职责就是在发生任何写入行为时,将过期时间重置为一个固定的值。
- expireAfterRead 返回 currentDuration,因为读取不是“写入”行为。根据“写入后过期”的策略定义,读取不应该影响过期时间,所以它通过返回原有时长来表达“无操作”。
使用场景:VarExpiration
的 put
和 compute
方法
这个类并非设计给用户直接使用的,而是作为 BoundedVarExpiration
实现中的一个“内部工具人”。BoundedVarExpiration
是 Policy.variableExpiration()
返回的策略对象,它允许用户对单个条目进行更精细的过期时间控制。
让我们看看 FixedExpireAfterWrite
在哪里被使用:
// ... existing code ...@SuppressWarnings("PreferJavaTimeOverload")final class BoundedVarExpiration implements VarExpiration<K, V> {
// ... existing code ...@Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {// 在这里创建了一个 FixedExpireAfterWrite 实例var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);// 将这个临时的 expiry 策略传递给 cache 的 put 方法return cache.put(key, value, expiry, onlyIfAbsent);}
// ... existing code ...@SuppressWarnings("unchecked")@Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {// 对于异步场景,也是先创建 FixedExpireAfterWritevar expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));var asyncValue = (V) CompletableFuture.completedFuture(value);var oldValueFuture = (CompletableFuture<V>) cache.put(key, asyncValue, expiry, /* onlyIfAbsent= */ false);return Async.getWhenSuccessful(oldValueFuture);}
// ... existing code ...}
// ... existing code ...
当调用 policy.variableExpiration().get().put(key, value, duration, unit)
时,会发生以下事情:
BoundedVarExpiration
的putSync
(或putAsync
) 方法被调用。- 该方法立即创建了一个
FixedExpireAfterWrite
的实例,并将传入的duration
和unit
作为构造参数。 - 然后,它将这个新创建的、临时的
expiry
对象传递给BoundedLocalCache
内部核心的put
方法。 - 核心
put
方法在处理这个条目时,就会使用这个临时的expiry
策略来计算新条目的过期时间。 - 一旦
put
操作完成,这个FixedExpireAfterWrite
实例的生命周期也就结束了。
为什么需要这个类?
这是一个非常精妙的设计,体现了对现有机制的复用。
FixedExpireAfterWrite
是一个一次性的“信使”或“指令条”。
想象一下,Caffeine 的核心数据操作方法(如内部的 put
和 compute
)是一个非常强大但很“专注”的工人。这个工人知道如何处理带有 Expiry
策略的条目,这是它的标准工作流程。
现在,你作为用户,通过 policy.variableExpiration().put(key, value, 5, MINUTES)
下达了一个非常具体的新指令:“把这个条目放进去,并让它5分钟后过期”。
Caffeine 的设计者面临一个选择:
- 坏选择:改造那个“工人”,教他一套全新的、不使用
Expiry
策略的流程来处理这种一次性指定时长的任务。这会让工人的工作变得复杂,容易出错。 - 好选择(Caffeine 的做法):把你的指令“翻译”成工人能听懂的标准格式。
FixedExpireAfterWrite
就是这个“翻译”的结果。它的工作流程是这样的:
打包指令: 当你调用
varExpiration.put(key, value, 5, MINUTES)
时,BoundedVarExpiration
类会立刻创建一个“信使”对象:new FixedExpireAfterWrite<>(5, MINUTES)
。这个信使对象身上只带了一个信息:“5分钟”。派遣信使:
BoundedVarExpiration
把这个信使(expiry
对象)连同key
和value
一起交给了核心的cache.put(...)
方法。工人读取指令:
cache.put
工人拿到key
,value
和这个信使后,开始工作。当它创建新条目需要知道过期时长时,它不关心这个信使是谁、从哪来,它只按照标准流程调用信使的expireAfterCreate(...)
方法。信使完成任务:
FixedExpireAfterWrite
的expireAfterCreate
方法被调用后,它甚至不看key
、value
这些参数,直接把自己携带的信息“5分钟”返回给工人。任务完成: 工人拿到“5分钟”这个时长,设置好条目的过期时间,然后
put
操作完成。这个一次性的“信使”对象也就完成了它的历史使命,很快会被垃圾回收。
总结一下:FixedExpireAfterWrite
是一个实现 Expiry
接口的、极其轻量的、一次性的对象。它的唯一作用就是充当一个载体,将一个具体的时间长度(如“5分钟”)传递给 Caffeine 内部通用的、基于 Expiry
接口的核心方法。这是一个非常优雅的设计,它复用了现有的强大功能,避免了代码冗余和逻辑复杂化。
总结
static final class FixedExpireAfterWrite<K, V>
是一个内部辅助类,它不是一个公开的API。
- 功能: 它实现
Expiry
接口,其行为是在创建或更新时返回一个固定的过期时长。 - 目的: 它的存在是为了适配和复用。它充当了一个“翻译官”或“信使”,将
VarExpiration.put(key, value, duration, unit)
这种“为单次操作指定时长”的调用,转换成BoundedLocalCache
核心方法所能理解的、标准的put(key, value, expiry)
调用。
这是一个展示了 Caffeine 内部如何通过巧妙的类设计和职责划分,以最小的代价实现强大功能的好例子。