You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Caffeine Cache指定过期时间,仅针对键值对的最新实例?

Caffeine Cache 过期时间重置问题

问题背景

我有一个键为String类型、值为LocalDateTime(记录插入时间)的Caffeine Cache,尝试通过实现Expiry接口管理过期时间。参考了如下StackOverflow代码片段:

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .expireAfter(new Expiry<Key, Graph>() {
      public long expireAfterCreate(Key key, Graph graph, long currentTime) {
        return 1000;
      }
      public long expireAfterUpdate(Key key, Graph graph, 
          long currentTime, long currentDuration) {
        return 1000;
      }
      public long expireAfterRead(Key key, Graph graph,
          long currentTime, long currentDuration) {
        return currentDuration;
      }
    })
    .build(key -> createExpensiveGraph(key));

我的核心需求是:当持续调用cache.put("A", LocalDateTime.now())更新同一个键时,希望过期计时器能重置——理论上如果无限调用该方法,这个键永远不会触发过期通知。实际业务场景中,这些键会先收到初始更新,之后每隔n秒会再次更新并写入缓存,我只希望当键的最新插入时间超过设定时限时,才触发过期,而不是初始插入后就开始计时。

解决方案

其实不需要手动实现Expiry接口,Caffeine内置的expireAfterWrite方法完全匹配这个需求:该策略会从最后一次写入操作(包括put、update等)开始计算过期时间,每次写入都会重置计时器,只有当最后一次写入后超过设定时长,才会触发过期。

1. 使用expireAfterWrite的实现

final Cache<String, LocalDateTime> cache = Caffeine.newBuilder()
        .executor(Runnable::run)
        .scheduler(Caffeine.scheduler().forScheduledExecutorService(Executors.newSingleThreadScheduledExecutor())) // 启用调度器确保过期及时触发
        .removalListener((key, value, cause) -> {
            if (cause == RemovalCause.EXPIRED) {
                System.out.printf("键=%s,值=%s 已过期%n", key, value);
            }
        })
        .expireAfterWrite(10, TimeUnit.SECONDS) // 最后一次写入后10秒过期
        .weakKeys()
        .weakValues()
        .build();

2. 手动实现Expiry接口的版本(可选)

如果一定要自定义Expiry逻辑,只需确保expireAfterCreate和expireAfterUpdate都返回设定的过期时长(单位:纳秒,Caffeine的ticker默认使用纳秒计时),这样每次创建或更新都会重置过期时间:

final Cache<String, LocalDateTime> cache = Caffeine.newBuilder()
        .executor(Runnable::run)
        .scheduler(Caffeine.scheduler().forScheduledExecutorService(Executors.newSingleThreadScheduledExecutor()))
        .removalListener((key, value, cause) -> {
            if (cause == RemovalCause.EXPIRED) {
                System.out.printf("键=%s,值=%s 已过期%n", key, value);
            }
        })
        .expireAfter(new Expiry<String, LocalDateTime>() {
            // 创建时设定过期时长:10秒(转成纳秒)
            @Override
            public long expireAfterCreate(String key, LocalDateTime value, long currentTime) {
                return TimeUnit.SECONDS.toNanos(10);
            }

            // 更新时重置过期时长:10秒
            @Override
            public long expireAfterUpdate(String key, LocalDateTime value, long currentTime, long currentDuration) {
                return TimeUnit.SECONDS.toNanos(10);
            }

            // 读取时不重置过期,保持剩余时长
            @Override
            public long expireAfterRead(String key, LocalDateTime value, long currentTime, long currentDuration) {
                return currentDuration;
            }
        })
        .weakKeys()
        .weakValues()
        .build();

测试验证代码

以下是验证逻辑的测试代码,每隔900毫秒更新一次键"a",由于过期时间设为10秒,持续更新期间"a"不会过期,而键"b"因未更新会在10秒后触发过期通知:

public class CacheExpiryTest implements Runnable {

    final Cache<String, LocalDateTime> cache = Caffeine.newBuilder()
            .executor(Runnable::run)
            .scheduler(createScheduler())
            .removalListener((key, value, cause) -> {
                if (cause == RemovalCause.EXPIRED) {
                    System.out.printf("键=%s,值=%s 已过期%n", key, value);
                }
            })
            .weakKeys()
            .weakValues()
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .build();

    private Scheduler createScheduler() {
        return Caffeine.scheduler().forScheduledExecutorService(Executors.newSingleThreadScheduledExecutor());
    }

    public void add(String key) {
        cache.put(key, LocalDateTime.now());
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new CacheExpiryTest());
        thread.start();
    }

    @Override
    public void run() {
        add("a");
        add("b");
        for (int i = 0; i < 100; i++) {
            System.out.println("更新键a 第" + (i+1) + "次");
            add("a");
            try {
                Thread.sleep(900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行这段代码可以看到:键"b"会在10秒左右输出过期日志,而键"a"因为每隔900毫秒被更新一次,永远不会触发过期。

内容的提问来源于stack exchange,提问作者Biscuit128

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 23:47:38