在使用 Caffeine 缓存时,经常会遇到一个问题:配置了基于时间的过期策略,为什么数据过期后还在内存里? 本文将深入解析 Caffeine 的时间老化触发原理,以及如何通过 Scheduler 实现准时清理。


一、Caffeine 的过期策略

Caffeine 支持三种基于时间的过期方式:

  • 写入后过期(expireAfterWrite)
    从最后一次写入开始计时,到达指定时间后过期。
  • 访问后过期(expireAfterAccess)
    从最后一次读/写访问开始计时,到达指定时间后过期。
  • 自定义过期(expireAfter)
    可以根据 key/value 灵活计算过期时间。

示例代码:

Cache<String, String> cache = Caffeine.newBuilder()
    .expireAfterWrite(Duration.ofMinutes(10))
    .build();

二、默认的惰性过期原理

默认情况下,Caffeine 的过期是 惰性触发 的。

  • 当你 getputcompute 等操作时,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 会在过期点附近被及时清理,而不是等到下一次访问。

优缺点

  • ✅ 内存更可控,过期数据不会堆积。
  • ✅ 过期更接近实时。
  • ⚠️ 增加了调度器线程和任务管理的开销。
  • ⚠️ 在超高吞吐场景下,性能可能略低于惰性过期。

五、实践建议

  1. 高性能优先
    如果你更关心吞吐量,且内存不是瓶颈,建议使用默认的惰性过期。
  2. 内存敏感
    如果内存有限,或者必须保证数据准时清理,可以启用 Scheduler
  3. 混合方式
    同时设置 maximumSize/Weight,保证即使没有调度器,也能依靠容量淘汰机制清理过期数据。

六、总结

  • Caffeine 默认采用 惰性过期,逻辑上过期但物理清理依赖访问或清理操作。
  • 访问其他 key 可能会触发清理,但不保证立即移除。
  • 通过 .scheduler(Scheduler.systemScheduler()) 可以让数据更准时地从内存中清除。
  • 惰性过期适合 追求性能 的场景,而启用 Scheduler 更适合 内存敏感 的场景。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注