咖啡因缓存延迟条目过期与创建后策略

Caffeine cache delayed entries expiration with after-creation policy

我正在使用 Caffeine 库进行缓存,创建条目后的过期时间间隔为 1 秒。 事实证明,条目过期有一些延迟,有时可能需要 2 倍以上的时间才能过期,而不是配置的 1 秒间隔。 根据我的实验,执行程序和调度程序线程计数配置对该延迟没有影响。

这是我的测试片段,我在其中测量特定条目在缓存中花费的时间并将其打印出来:

private final Cache<String, LinkedList<Pair<Long, Long>>> groupedEvents = Caffeine.newBuilder()
    .expireAfter(new Expiry<String, LinkedList<Pair<Long, Long>>>() {
        public long expireAfterCreate(String key, LinkedList<Pair<Long, Long>> val, long currentTime) {
            return TimeUnit.SECONDS.toNanos(1);
        }
        public long expireAfterUpdate(String key, LinkedList<Pair<Long, Long>> val, long currentTime, long currentDuration) {
            return currentDuration;
        }
        public long expireAfterRead(String key, LinkedList<Pair<Long, Long>> val, long currentTime, long currentDuration) {
            return currentDuration;
        }
    })
    .scheduler(Scheduler.forScheduledExecutorService(Executors.newScheduledThreadPool(4)))
    .executor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2))
    .removalListener((key, events, cause) -> {
        long timeInCache = System.currentTimeMillis() - events.get(0).getLeft();
        System.out.println(String.format("key %s, values count: %s, timeInCache: %s ms",
                key, events.size(), timeInCache));
    }).build(); 

@Test
public void test() throws InterruptedException {

    for (int i = 0; i < 1000; i++) {
        Thread.sleep(30);
        String key = String.valueOf(new Random().nextInt(30));

        groupedEvents.asMap().compute(key, (s, values) -> {
            if (values == null) {
                values = new LinkedList<>();
            }
            values.add(Pair.of(System.currentTimeMillis(), 123L));
            return values;
        });
    }

输出如下:

key 10, values count: 1, timeInCache: 1223 ms
key 0, values count: 3, timeInCache: 1470 ms
key 20, values count: 1, timeInCache: 2295 ms
key 15, values count: 2, timeInCache: 2325 ms
key 16, values count: 1, timeInCache: 2023 ms
key 23, values count: 1, timeInCache: 1566 ms
key 18, values count: 1, timeInCache: 2052 ms
key 14, values count: 2, timeInCache: 1079 ms
key 3, values count: 3, timeInCache: 1628 ms
key 4, values count: 2, timeInCache: 1109 ms
key 2, values count: 2, timeInCache: 1841 ms
key 17, values count: 1, timeInCache: 1535 ms
key 13, values count: 2, timeInCache: 1011 ms
key 7, values count: 1, timeInCache: 1471 ms
key 12, values count: 1, timeInCache: 1441 ms

如您所见,负载不高,每秒大约添加 33 个条目(基于 Thread.sleep(30)),但对于某些条目,需要创建后最多 ~2300 毫秒而不是所需的 1 秒过期,并且该延迟对我的应用程序至关重要,因为最终用户不会在 1-1.3 秒的间隔内获取我的数据。

是否有机会调整条目逐出时间以减少延迟? 我正在使用最新的 'com.github.ben-manes.caffeine:caffeine:2.8.5'' 版本

Caffeine.scheduler:

中所述,计划执行的最小间隔为 ~1.07 秒

Specifies the scheduler to use when scheduling routine maintenance based on an expiration event. This augments the periodic maintenance that occurs during normal cache operations to allow for the prompt removal of expired entries regardless of whether any cache activity is occurring at that time. By default, {@link Scheduler#disabledScheduler()} is used.

The scheduling between expiration events is paced to exploit batching and to minimize executions in short succession. This minimum difference between the scheduled executions is implementation-specific, currently at ~1 second (2^30 ns). In addition, the provided scheduler may not offer real-time guarantees (including {@link ScheduledThreadPoolExecutor}). The scheduling is best-effort and does not make any hard guarantees of when an expired entry will be removed.

持续时间长是由于条目在上次维护后的某个时间到期 运行。这可能导致最长寿命为步速间隔的 2 倍,例如当下一个过期条目在未来只有 1ns 时。这延长了一点,因为底层系统不提供实时保证,所以它在你的例子中又损失了十几毫秒。

为了提供 O(1) 过期时间,缓存将条目保存在 timing wheel 中,其中最小存储桶持续时间为 1.07 秒。为了避免不断地重新 运行 维护,它按照这个最小阈值来调整执行速度。即使删除了这个起搏器,由于桶的大小,最坏情况下的生命周期仍将存在,因为准备过期的条目必须等待整个桶被驱逐。

因此,减少这种最坏情况的唯一方法是添加一个使用更短持续时间的轮子。该轮子可能有 2 个桶,时间为 2^29ns(530 毫秒),4 个桶为 2^28ns(268 毫秒),等等。最坏情况下的延迟将是桶的持续时间,因此我们必须确定可接受的新值。如果您想探索此选项,请打开 Github 问题。

这解释了技术细节和可能的改进。然而,如果另一个用户想要更精细的分辨率,这是一种平衡行为。在极端情况下,这会导致完美的解决方案,需要 O(lg n) 成本来维护排序顺序,这会降低非常大的缓存的性能。当我们尝试平衡设计权衡时,我们可能会导致某些场景不适合,考虑更适合您权衡的替代方案是完全可以的。

例如,您可能更愿意将每个条目插入 ScheduledExecutorService 而不是依赖 Caffeine 的过期时间。您的代码将在计算中安排删除。在我的测试中,最坏情况下的额外延迟为 13 毫秒 运行s.

groupedEvents.asMap().compute(key, (s, values) -> {
  if (values == null) {
    scheduledExecutor.schedule(
      () -> groupedEvents.invalidate(key), 1, TimeUnit.SECONDS);
    values = new LinkedList<>();
  }
  values.add(Pair.of(System.currentTimeMillis(), 123L));
  return values;
});