如何为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
相关产品推荐
相关产品推荐

