在使用 Caffeine 缓存时,经常会遇到一个问题:配置了基于时间的过期策略,为什么数据过期后还在内存里? 本文将深入解析 Caffeine 的时间老化触发原理,以及如何通过 Scheduler
实现准时清理。
一、Caffeine 的过期策略
Caffeine 支持三种基于时间的过期方式:
- 写入后过期(expireAfterWrite)
从最后一次写入开始计时,到达指定时间后过期。 - 访问后过期(expireAfterAccess)
从最后一次读/写访问开始计时,到达指定时间后过期。 - 自定义过期(expireAfter)
可以根据 key/value 灵活计算过期时间。
示例代码:
Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(10)) .build();
二、默认的惰性过期原理
默认情况下,Caffeine 的过期是 惰性触发 的。
- 当你
get
、put
、compute
等操作时,Caffeine 会检查 entry 是否已过期; - 如果过期,则逻辑上返回
null
,并可能触发一次异步清理; - 如果 entry 从未再被访问,就会一直占用内存,直到触发清理或调用
cache.cleanUp()
。
👉 也就是说:
数据到期了不会立刻删除,只是在下一次访问或维护时才真正清理。
这种设计避免了为每个 entry 开定时器的巨大开销,能保证极高的性能。
三、访问其他 key 能否触发过期?
答案是 可以,但不保证。
例如:
cache.put("key1", "v1"); Thread.sleep(3000); // key1 已过期 cache.put("key2", "v2"); // 访问另一个 key
此时:
key1
已经过期,cache.getIfPresent("key1")
会返回null
;- 但是
key1
物理上是否立刻被移除,不一定,取决于维护线程的清理进度。
Caffeine 为了性能采用分批清理,避免每次访问都全表扫描。
四、引入 Scheduler 的定时清理
如果希望数据到期后准时删除,可以启用调度器:
Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(10)) .scheduler(Scheduler.systemScheduler()) .build();
原理
put
数据时,会计算出过期时间并注册到Scheduler
;- 到达过期时间时,调度器会异步触发清理任务;
- 这样 entry 会在过期点附近被及时清理,而不是等到下一次访问。
优缺点
- ✅ 内存更可控,过期数据不会堆积。
- ✅ 过期更接近实时。
- ⚠️ 增加了调度器线程和任务管理的开销。
- ⚠️ 在超高吞吐场景下,性能可能略低于惰性过期。
五、实践建议
- 高性能优先
如果你更关心吞吐量,且内存不是瓶颈,建议使用默认的惰性过期。 - 内存敏感
如果内存有限,或者必须保证数据准时清理,可以启用Scheduler
。 - 混合方式
同时设置maximumSize/Weight
,保证即使没有调度器,也能依靠容量淘汰机制清理过期数据。
六、总结
- Caffeine 默认采用 惰性过期,逻辑上过期但物理清理依赖访问或清理操作。
- 访问其他 key 可能会触发清理,但不保证立即移除。
- 通过
.scheduler(Scheduler.systemScheduler())
可以让数据更准时地从内存中清除。 - 惰性过期适合 追求性能 的场景,而启用 Scheduler 更适合 内存敏感 的场景。