当前位置: 首页 > news >正文

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 计算其存活时长。
  • 灵活性: 允许为每个条目设置不同的、动态变化的过期策略,而不是整个缓存一刀切。
  • 易用性: 通过 creatingwritingaccessing 三个静态工厂方法和对应的内部实现类,为最常见的使用模式提供了便捷的创建方式。

AsyncExpiry

AsyncExpiry 是 Async 工具类中的一个内部类,它的作用是将一个普通的 Expiry<K, V> 策略适配到异步缓存 AsyncCache<K, V> 的场景中

在 AsyncCache 中,缓存的值(Value)不再是普通的对象 V,而是一个 CompletableFuture<V>。这就带来了一个核心问题:当一个 CompletableFuture 还没有计算完成时,它的过期策略应该如何处理? AsyncExpiry 就是为了解决这个问题而设计的。

AsyncExpiry 的核心思想是,它将一个缓存条目的生命周期看作两种状态:

  1. 加载中(Pending)CompletableFuture 还没有完成(isDone() 为 false)。
  2. 已就绪(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):
    1. 它会从 future 中 join() 出最终的计算结果 V
    2. 然后调用被委托的 delegate.expireAfterCreate() 方法,传入真实的 key 和 value,让用户定义的策略来计算过期时间。
    3. 最后,确保返回的时长不会超过一个内部设定的最大值 MAXIMUM_EXPIRY
  • 当 Future 仍在加载中 (isReady(future) 为 false):
    1. 它直接返回一个静态常量 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) 时,会发生以下事情:

  1. BoundedVarExpiration 的 putSync (或 putAsync) 方法被调用。
  2. 该方法立即创建了一个 FixedExpireAfterWrite 的实例,并将传入的 duration 和 unit 作为构造参数。
  3. 然后,它将这个新创建的、临时的 expiry 对象传递给 BoundedLocalCache 内部核心的 put 方法。
  4. 核心 put 方法在处理这个条目时,就会使用这个临时的 expiry 策略来计算新条目的过期时间。
  5. 一旦 put 操作完成,这个 FixedExpireAfterWrite 实例的生命周期也就结束了。

为什么需要这个类?

这是一个非常精妙的设计,体现了对现有机制的复用。

FixedExpireAfterWrite 是一个一次性的“信使”或“指令条”。

想象一下,Caffeine 的核心数据操作方法(如内部的 put 和 compute)是一个非常强大但很“专注”的工人。这个工人知道如何处理带有 Expiry 策略的条目,这是它的标准工作流程。

现在,你作为用户,通过 policy.variableExpiration().put(key, value, 5, MINUTES) 下达了一个非常具体的新指令:“把这个条目放进去,并让它5分钟后过期”。

Caffeine 的设计者面临一个选择:

  1. 坏选择:改造那个“工人”,教他一套全新的、不使用 Expiry 策略的流程来处理这种一次性指定时长的任务。这会让工人的工作变得复杂,容易出错。
  2. 好选择(Caffeine 的做法):把你的指令“翻译”成工人能听懂的标准格式。

FixedExpireAfterWrite 就是这个“翻译”的结果。它的工作流程是这样的:

  1. 打包指令: 当你调用 varExpiration.put(key, value, 5, MINUTES) 时,BoundedVarExpiration 类会立刻创建一个“信使”对象:new FixedExpireAfterWrite<>(5, MINUTES)。这个信使对象身上只带了一个信息:“5分钟”。

  2. 派遣信使BoundedVarExpiration 把这个信使(expiry 对象)连同 key 和 value 一起交给了核心的 cache.put(...) 方法。

  3. 工人读取指令cache.put 工人拿到 keyvalue 和这个信使后,开始工作。当它创建新条目需要知道过期时长时,它不关心这个信使是谁、从哪来,它只按照标准流程调用信使的 expireAfterCreate(...) 方法。

  4. 信使完成任务FixedExpireAfterWrite 的 expireAfterCreate 方法被调用后,它甚至不看 keyvalue 这些参数,直接把自己携带的信息“5分钟”返回给工人。

  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 内部如何通过巧妙的类设计和职责划分,以最小的代价实现强大功能的好例子。


文章转载自:

http://gadaWMvn.pfntr.cn
http://a8BZtYZm.pfntr.cn
http://NfTWm5Zj.pfntr.cn
http://R7LQKJUW.pfntr.cn
http://Ep7KTiHi.pfntr.cn
http://2QV9YHak.pfntr.cn
http://JhE9vPut.pfntr.cn
http://XjUfxms7.pfntr.cn
http://L7cjB3DO.pfntr.cn
http://0O6lYOH3.pfntr.cn
http://KKSECBbe.pfntr.cn
http://bRvJEbTy.pfntr.cn
http://ZK46VKY7.pfntr.cn
http://DCRCPlRd.pfntr.cn
http://HZKg4HD3.pfntr.cn
http://1YN4KNNw.pfntr.cn
http://esBnLT66.pfntr.cn
http://eyp7xLG4.pfntr.cn
http://x1jXMtPi.pfntr.cn
http://fqWrZp5S.pfntr.cn
http://GsyjrgNu.pfntr.cn
http://BF8Uzo5H.pfntr.cn
http://VUjcxw29.pfntr.cn
http://X2wickVc.pfntr.cn
http://MeLU0RvP.pfntr.cn
http://mjrZpWhv.pfntr.cn
http://JBFqNSVG.pfntr.cn
http://uGyLaCb0.pfntr.cn
http://veEfhPTT.pfntr.cn
http://MgWBmC4G.pfntr.cn
http://www.dtcms.com/a/388361.html

相关文章:

  • 【C++项目】C++11重构muduo库
  • 如何选择靠谱的防伪溯源系统公司?
  • 线程池 相关知识
  • 搭建CI/CD 流水线简单说明
  • 大Key与热Key详解:概念、危害与解决方案
  • Java中的自动拆装箱原理
  • Android 入门笔记(2)
  • 程序员内功之成长性思维
  • vLLM 和 SGLang 是两个近年来备受关注的开源项目
  • CMake进阶: 路径处理指令join_paths和cmake_path
  • 算法简略速记手册
  • C语言(长期更新)第17讲内存函数
  • 【CSP-S】 基础知识与编程环境
  • Python HTTPS 教程 如何发送 HTTPS 请求、解决证书错误、实现抓包与网络调试全攻略
  • 【Cesium 开发实战教程】第五篇:空间分析实战:缓冲区、可视域与工程测量
  • 告别塑料感!10分钟学会基础材质调节
  • CSS Modules 和 CSS-in-JS比较
  • threejs(三)模型对象、材质
  • (自用)vscode正则表达式(正则表达式语法大全)vocode正则化(注意正则化和正则表达式不是一个概念)
  • Node.js:重新定义全栈开发的JavaScript运行时
  • @PropertySource 注解学习笔记
  • 安徽Ecovadis认证辅导怎么做呢?
  • 【完整源码+数据集+部署教程】太阳能面板缺陷分割系统: yolov8-seg-C2f-REPVGGOREPA
  • 什么是直播美颜SDK?人脸识别与实时渲染的技术解析
  • RabbitMQ-MQTT即时通讯详解
  • AI辅助论文写作:如何成为真正的“AI Native学者”?
  • Frida 实战:Android JNI 数组 (jobjectArray) 操作全流程解析
  • 腾讯正式发布全新一代智能驾驶地图9.0
  • 鸿蒙应用开发之装饰器大总结 —— 从语法糖到全场景跨语言运行时的全景视角
  • 论文阅读:EMNLP 2024 Humans or LLMs as the Judge? A Study on Judgement Bias