Caffeine Expiry 中如何设置多个过期标准?

Scr*_*yos 3 java caching redis caffeine

我正在使用 Caffeine v2.8.5,我想创建一个具有可变到期时间的缓存,基于:

  • 值的创建/更新以及
  • 该值的最后一次访问(读取)。

无论先发生什么都应该触发该条目的删除。


缓存将成为三层值解析的一部分:

  1. 密钥存在于咖啡因缓存中
    • 使用这个值
    • 刷新访问/读取过期
  2. 密钥存在于 Redis 数据库中
  3. 该密钥既不存在于内部缓存也不存在于 Redis 中
    • 从外部 REST API 请求值
    • 将此值存储在 Redis 数据库中,固定期限为 30 天
    • 将此值存储在 Caffeine 缓存中,固定期限为 30 天

Redis用作全局缓存,以便多个应用程序/实例可以共享缓存数据,但这种解析经常发生,以至于它不能用于每个请求,因此需要另一个缓存层。

根据请求时间,请求的数据具有不同的 TTL。因此,虽然当我们请求 REST API 时过期时间可能是固定的,并且过期时间是在 Redis 中设置的,但在 Caffeine 中该时间将是动态的,因为过期时间基于 Redis 密钥的剩余 TTL。

情况 (2) 和 (3) 已经在我的 Caffeine 缓存的 CacheLoader 中得到解决(我在通读模式下使用缓存)。为了控制过期,我已经发现,我必须使用高级 Expiry API,并且我还研究了类似的问题,例如(Specify expiry for an Entry)(Expire cached value after create time)。所以我为我的键想出了一个包装对象,如下所示:

import lombok.Value;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.time.Instant;

@Value
public class ExpiringValue<ValueType> {

    @Nullable
    private final ValueType value;
    @NotNull
    private final Instant validUntil;
}
Run Code Online (Sandbox Code Playgroud)

和像这样的到期

import com.github.benmanes.caffeine.cache.Expiry;
import org.jetbrains.annotations.NotNull;

import java.time.Duration;
import java.time.Instant;

public final class ValueBasedExpiry<KeyType, ValueType extends ExpiringValue<?>> implements Expiry<KeyType, ValueType> {

    @Override
    public long expireAfterCreate(
        @NotNull final KeyType key,
        @NotNull final ValueType value,
        final long currentTime
    ) {
        return Duration.between(Instant.now(), value.getValidUntil()).toNanos();
    }

    @Override
    public long expireAfterUpdate(
        @NotNull final KeyType key,
        @NotNull final ValueType value,
        final long currentTime,
        final long currentDuration
    ) {
        return currentDuration;
    }

    @Override
    public long expireAfterRead(
        @NotNull final KeyType key,
        @NotNull final ValueType value,
        final long currentTime,
        final long currentDuration
    ) {
        return currentDuration;
    }
}
Run Code Online (Sandbox Code Playgroud)

我的用例的不同之处在于,我希望有一个基于该值的上次访问的第二个到期标准。因此,如果一个小时内没有提出请求,我想提前删除该条目。而如果频繁访问,最终会在 TTL 为零后被删除。

我将如何实施第二个标准?我不知道如何获取上次访问条目的时间。该接口似乎没有提供这样的值。我也研究过这个问题。根据条目已排序到的调度程序存储桶,定期调用/重新评估这些方法是否正确?

Scr*_*yos 8

我对Expiry如何工作的最大误解是,我认为 Expiry 的方法会定期触发并重新评估。我正在回答我自己的问题,以防有人从他们的研究中得到同样的印象。

仅当执行了相应方法名称的操作后,才会调用Expiry内的方法(因此仅更新值)。因此,例如expireAfterRead(K, V, long, long),仅当每次在缓存中读取此键值映射时才会调用。

因此,如果映射在创建后永远不会执行任何操作(不读取或更新),则只会expireAfterCreate(K, V, long)调用该方法一次。这就是为什么所有方法都应始终返回剩余持续时间,但不必考虑上次读取条目的时间,因为该时刻就是现在(如 中Instant.now()),因此expireAfterRead(K, V, long, long)调用 。

正如 @BenManes 在评论中指出的那样,我最初问题的正确解决方案正在返回

Math.min(TimeUnit.HOURS.toNanos(1), Duration.between(Instant.now(), value.getValidUntil()).toNanos())
Run Code Online (Sandbox Code Playgroud)

在 Expiry 的所有三种方法中。


并回答我在帖子中的另外两个问题:

我如何获得最后一次访问条目的时间?Instant.now()在方法中 调用(例如)expireAfterRead(K, V, long, long)。如果您还想在外部或在其他过期方法中拥有该值,则始终可以选择使用易失性字段将此值存储在 ExpiringValue 中。

根据条目已排序到的调度程序存储桶,定期调用/重新评估这些方法是否正确? 不会。如上所述,只有执行相应操作后,Expiry 中的方法才会被调用。这些方法不会定期触发或重新评估。