如何创建从内存到数据库的写后缓存?

时间:2018-10-17 08:05:41

标签: java spring caching

我的目标是将数据内存缓存60秒。从缓存中再次读取该条目后,我想将其从缓存中删除(仅允许单次读取)。

如果这60年代同时到期,并且该条目在高速缓存中仍然可用,那么我想在该条目的后面写入数据库。

是否有任何现有的技术/弹簧/ Apache框架已经提供了这种缓存? (旁注:对于这样一个简单的用例,我不想使用诸如redis,ehcache等复杂的库。)

如果手动设置,我将执行以下操作。但是也许还有更好的选择?

@Service
public class WriteBehindCache {
    static class ObjectEntry {
        Object data;
        LocalDateTime timestamp;

        public ObjectEntry(Object data) {
              this.data = data;
              timestamp = LocalDateTime.now();
        }
    }

    Map<String, ObjectEntry> cache = new ConcurrentHashMap<>();

    //batch every minute
    @Scheduled(fixedRate = 60000)
    public void writeBehind() {
        LocalDateTime now = LocalDateTime.now();
        List<ObjectEntry> outdated = cache.values()
             .filter(entry -> entry.getValue().timestamp.plusSeconds(60).isBefore(now))
             .collect(Collectors.toList());

        databaseService.persist(outdated);
        cache.removeAll(outdated); //pseudocode
    }

    //always keep most recent entry
    public void add(String key, Object data) {
        cache.put(key, new ObjectEntry(data));
    }

    //fallback lookup to database if cache is empty
    public Object get(String key) {
        ObjectEntry entry = cache.remove(key);
        if (entry == null) {
            entry = databaseService.query(key);
            if (entry != null)  databaseService.remove(entry);
        }
        return entry;
    }
}

1 个答案:

答案 0 :(得分:1)

您的解决方案有两个问题:

  • 您正在进行顺序扫描以进行持久性扫描,当条目很多时​​,这会变得很昂贵
  • 代码具有竞争条件
  • 由于竞赛条件,该代码无法满足您的要求。可以构造并发访问序列,在该序列中,从缓存中删除条目,但也将其写入数据库中
  

是否有任何现有的技术/弹簧/ Apache框架已经提供了这种缓存? (旁注:对于这样一个简单的用例,我不想使用诸如redis,ehcache等复杂的库。)

我认为您可以基于ConcurrentHashMap解决并发问题。但是我不知道超时的优雅方法。不过,可能的解决方案是使用缓存库。我想提供一个基于cache2k的示例,该示例并不繁琐(大约40万个jar),并且还有其他很好的用例。另外,Spring缓存抽象也得到了很好的支持。

public static class WriteBehindCache {

  Cache<String, Object> cache = Cache2kBuilder.of(String.class, Object.class)
    .addListener((CacheEntryExpiredListener<String, Object>) (cache, entry)
      -> persist(entry.getKey(), entry.getValue()))
    .expireAfterWrite(60, TimeUnit.SECONDS)
    .build();

  public void add(String key, Object data) {
    cache.put(key, data);
  }

  public Object get(String key) {
    return cache.invoke(key, e -> {
      if (e.exists()) {
        Object v = e.getValue();
        e.remove();
        return v;
      }
      return loadAndRemove(e.getKey());
    });
  }


  // stubs
  protected void persist(String key, Object value) {
  }

  protected Object loadAndRemove(String key) {
    return null;
  }

}

通过这种连接,高速缓存会阻止对一个条目的并发操作,因此可以确定一次只有一个数据库操作针对一个条目运行。

您可以使用其他缓存库以类似方式进行操作。使用JCache / JSR107 API,代码看起来几乎相同。

更“轻松”的方法是使用贾特曼(Jhalterman)的expiringmap

我个人认为,每个开发人员工具箱中都应该有一个缓存。但是,我是cache2k的作者。当然,我需要这么说。