【问题标题】:Caching with eviction based on timestamp基于时间戳的驱逐缓存
【发布时间】:2014-12-18 21:41:50
【问题描述】:

我的服务端点每分钟都会收到一个指标列表及其时间戳。如果指标通过某些条件,我们需要将它们存储在缓存中,以便以后可以访问它们。该服务的访问功能是 -

List<Metrics> GetAllInterestingMetrics5Mins();
List<Metrics> GetAllInterestingMetrics10Mins();
List<Metrics> GetAllInterestingMetrics30Mins();

我当前的解决方案是使用 3 个 Guava 缓存,并将基于时间的驱逐设置为 5、10 和 15 分钟。当有人调用上述函数之一时,我会从相关缓存中返回所有指标。

这有两个问题 -

  1. Guava 缓存开始时间用于驱逐基于值何时放入缓存(或访问,取决于设置)。现在,指标可能会延迟,因此时间戳会早于指标放入缓存的时间。
  2. 我不喜欢我必须创建 3 个缓存,当一个 30 分钟的缓存就足够时,它会增加内存占用和缓存处理的复杂性。

有没有办法在 Guava 或任何其他开箱即用的缓存解决方案中解决这两个问题?

【问题讨论】:

    标签: java spring caching guava


    【解决方案1】:

    Guava 和 EHCache 等缓存解决方案与您尝试实现的解决方案之间存在特殊差异。这些缓存的唯一目的是以与 getter 函数相同的方式工作。因此,缓存旨在通过其键检索单个元素并将其存储以供进一步使用;停止使用后将其驱逐。

    例如

    @Cacheable
    public Object getter(String key){
    ...
    }
    

    这就是为什么从缓存中获取一整套对象感觉有点像强制缓存和逐出策略的工作方式与其最初的目的不同。

    您需要的不是 Guava 缓存(或其他缓存解决方案),而是一个可以通过计时器函数一次性全部清除的集合。可悲的是,番石榴现在不提供。您仍然需要应用程序提供的计时器功能,该功能将从缓存中删除所有现有元素。

    所以,我的建议如下:

    即使 Guava 可以按照您希望的方式运行,您也会发现您没有使用使 Guava 真正有价值的功能,并且您正在“强迫”它以不同的方式运行。所以我建议你忘记 Guava 的实现,考虑使用 AbstractMap 类的一个特化,以及一个每 N 秒驱逐其内容的计时器函数。

    这样您就可以将所有条目都放在一个缓存中,而不必担心时间戳与条目添加到缓存中的时间之间的差异。

    【讨论】:

      【解决方案2】:

      关于主题1:

      只是一个旁注:请不要混淆到期和驱逐。过期意味着该条目可能不再由缓存返回,并且可能发生在指定的时间点或持续时间之后。驱逐是释放资源的动作,条目从缓存中删除。到期后,驱逐可能在同一时间或之后发生。

      所有常见的缓存产品都不支持精确的,也就是“时间点”,到期。我们的应用程序中经常需要这个用例,所以我在cache2k 上花费了一些精力来支持它。

      这是 cache2k 的蓝图:

      static class MetricsEntry {
      
        long nextUpdate;
        List<Metrics> metrics;
      
      }
      
      static class MyEntryExpiryCalculator implements EntryExpiryCalculator<Integer, MetricsEntry> {
        @Override
        public long calculateExpiryTime(Integer _key, MetricsEntry _value, long _fetchTime, CacheEntry _oldEntry) {
          return _value.nextUpdate;
        }
      }
      
      Cache createTheCache() {
        Cache<Integer, MetricsEntry> cache =
          CacheBuilder.newCache(Integer.class, MetricsEntry.class)
            .sharpExpiry(true)
            .entryExpiryCalculator(new MyEntryExpiryCalculator())
            .source(new MySource())
            .build();
         return cache;
      }
      

      如果您在度量对象中有时间参考,则可以使用它,并且可以省略额外的条目类。 sharpExpiry(true) 指示 cache2k 的确切到期时间。如果您忽略它,到期时间可能会延迟几毫秒,但访问时间会稍微快一些。

      关于主题 2:

      直接的方法是使用间隔分钟作为缓存键。

      这里是一个缓存源(又名缓存加载器),它严格返回上一个区间的指标:

      static class MySource implements CacheSource<Integer, MetricsEntry> {
        @Override
        public MetricsEntry get(Integer interval)  {
          MetricsEntry e = new MetricsEntry();
          boolean crossedIntervalEnd;
          do {
            long now = System.currentTimeMillis();
            long intervalMillis = interval * 1000 * 60;
            long startOfInterval = now % (intervalMillis);
            e.metrics = calculateMetrics(startOfInterval, interval);
            e.nextUpdate = startOfInterval + intervalMillis;
            now = System.currentTimeMillis();
            crossedIntervalEnd = now >= e.nextUpdate;
          } while (crossedIntervalEnd);
          return e;
        }
      }
      

      如果您在 10:07 发出请求,这将返回 10:00-10:05 的指标。

      如果你只是想立即计算过去区间的指标,那就更简单了:

      static class MySource implements CacheSource<Integer, MetricsEntry> {
        @Override
        public MetricsEntry get(Integer interval)  {
          MetricsEntry e = new MetricsEntry();
          long intervalMillis = interval * 1000 * 60;
          long startOfInterval = System.currentTimeMillis();
          e.metrics = calculateMetrics(startOfInterval, interval);
          e.nextUpdate = startOfInterval + intervalMillis;
          return e;
        }
      }
      

      使用缓存源优于put()。 cache2k 是阻塞的,所以如果一个指标有多个请求,只开始一个指标计算。

      如果您不需要精确到毫秒的过期时间,您也可以使用其他缓存。您需要做的是将计算指标所需的时间存储在缓存值中,然后相应地更正到期持续时间。

      祝你好运!

      【讨论】:

        【解决方案3】:

        您是否考虑过使用Deque 之类的东西?只需将指标放入队列中,当您想要检索最后 N 分钟的指标时,只需从最后添加的最新内容开始,然后获取所有内容,直到找到 > N 分钟前的指标。您可以以类似的方式从另一端逐出太旧的条目。 (从您的问题中我不清楚Cache 的键/值方面与您的问题有何关系。)

        【讨论】:

        • 但是我必须写一个线程来定期删除旧条目。使用开箱即用的驱逐缓存的全部目的不是为了避免这种情况吗?你是对的,这个问题没有键/值方面。我只是使用哈希码作为键,因为我找不到任何基于时间的驱逐队列。
        • 当您添加新条目时,或者可能在调用您的读取条目的方法之一时,您不能只删除过期的条目吗?这基本上就是 Cache 所做的:它删除所有写入(对于正在写入的段)和偶尔读取的驱逐条目。
        • @Rohitchauhan 从缓存中逐出只是缓存的“微小附加功能”,您无法有效地实现它。重点是缓存;缺少键意味着缺少使用缓存的理由。首先,我想建议使用一个 30 分钟的缓存并手动过滤,但 DequePriorityQueue 似乎更有意义。添加驱逐是相当微不足道的。
        猜你喜欢
        • 2014-10-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-09
        • 1970-01-01
        • 1970-01-01
        • 2018-08-20
        相关资源
        最近更新 更多