首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >内存键值缓存是否包含引用的值?

问内存键值缓存是否包含引用的值?
EN

Stack Overflow用户
提问于 2021-10-24 16:59:33
回答 2查看 224关注 0票数 2

我有点不清楚如何以及是否使用内存中的缓存。

我正在开发一个Java应用程序(Min克拉夫特插件),它创建了一个中世纪的MMO类型的游戏,其中有很多对象(城镇、结构和具有特定属性的项目等)。

这些对象中有许多是经过大量读取和修改的,这使我研究了一种缓存解决方案,以防止从MySQL数据库中开始获取这些对象(这可能不是MMO的最佳解决方案,但我现在想继续使用它)。

因此,对于我的具体情况,我认为内存中的缓存方法似乎是正确的选择。我一直在搜索和挖掘大量关于缓存和缓存提供程序的信息,但我仍然不清楚内存中的缓存是否需要每次修改对象时检索和缓存对象。

这是我需要理解的一个重要部分,因为有些对象是不断修改的。让我们以一个城镇大门为例。“门”对象具有一个健康属性,当玩家损坏该门时,该属性将发生更改。根据我的理解,每次玩家损坏大门时,获取gate、更改health和缓存修改过的对象似乎有点麻烦。特别是考虑到大多数门禁对象的默认健康值为500,玩家的伤害输出可能是20。这意味着,如果玩家想要销毁这个门对象,那么这个对象必须在几分钟内被取出、修改和缓存25次。

是否有可能只从缓存中获取对象(假设对象已经从DB中检索并缓存),修改对象并将其保留下来?当然,这意味着缓存将保存对对象的引用,而当修改时,缓存将引用修改的实例。如果这是可能的话,这是一个良好的方法性能?

总结我的问题:

  1. 考虑到我的情况(很多对象经常被修改),内存中的(键值)缓存是否符合逻辑?
  2. 在缓存中保存对缓存对象的引用是否可能和有效(从性能上讲)?

提前感谢您的帮助!

EN

回答 2

Stack Overflow用户

发布于 2021-10-27 10:43:56

有趣的问题,因为它涉及许多方面的性能工程,并发编程,特别是一致性通过性能权衡。

在Hibernate中启用缓存并在缓存后配置写是可能的,例如,使用EHCache。但是,由于Hibernate和数据库是为事务性工作负载和一致性而设计的,因此仍然存在相当大的开销。

我将介绍一个基于cache2k的潜在最优解决方案。我确实利用了cache2k中其他缓存中不存在的一些特性,但是,这里使用的一些技术可以与其他缓存实现一起使用,比如EHCache、Guava Cache、咖啡因或支持引用存储的JCache/ use 107。

因为您在单个服务器上运行,所以在内存中处理数据是最有效的。写入可以跳过或延迟,因为您没有像银行帐户这样的事务性数据。在服务器崩溃的情况下,可以容忍少量的状态更新。在发生崩溃时,它总是在更新性能和潜在数据丢失之间进行权衡。

可以在映射或缓存中保存当前状态,然后更新现有对象。示例:

代码语言:javascript
复制
class Gate {
  final AtomicInteger health = new AtomicInteger(100);
}
Cache<Id, Gate> cache = ....;

void decreaseHealth(Id id, int damage) {
  Gate gate = cache.get(id);
  gate.health.addAndGet(-damage);
}

我介绍了稍后与数据库交互的附加代码,并首先关注内存中的更新。

如果使用映射而不是缓存,则需要使用线程安全的映射,如ConcurrentHashMap。

由于更改可能同时发生,所以您需要使用一种方法来原子地更新健康状况。上面我使用了AtomicInteger。另一种可能是原子更新器或var句柄。如果您只是更新单个值,这是最有效的方法,因为它转换为硬件上的单个CAS操作。如果更新对象中的多个值,请在缓存/映射条目上使用锁、synchronized或原子操作。示例:

代码语言:javascript
复制
class Gate {
  int health = 100;
  // ....
}
cache.asMap().compute(id, (unused, gate) -> {
   gate.health -= damage;
   if (gate.health == 0) {
     // more changes to the object if destroyed totally
   }
   return gate;
 });

下面是一种基于cache2k的工作解决方案的想法,该解决方案可以在发生重大更改时延迟执行。

代码语言:javascript
复制
  // mocks
  class Id {}
  Gate readFromDb() { return null; }
  void writeDb(Gate g) { }

  class Gate {
    final AtomicInteger health = new AtomicInteger(100);
    volatile boolean writeScheduled = false;
    final AtomicInteger persistentHealth = new AtomicInteger(100);
    boolean isDirty() {
      return persistentHealth.get() != health.get();
    }
  }

  Cache<Id, Gate> cache =
    new Cache2kBuilder<Id, Gate>() {}
      .loader((id, l, cacheEntry) -> {
        if (cacheEntry == null) { return readFromDb(); }
        Gate gate = cacheEntry.getValue();
        return gate;
      })
      .addListener((CacheEntryExpiredListener<Id, Gate>) (cache, cacheEntry) -> {
        writeIfModified(cacheEntry.getValue());
      })
      .refreshAhead(true)
      .keepDataAfterExpired(true)
      .expireAfterWrite(5, TimeUnit.MINUTES)
      .loaderExecutor(Executors.newFixedThreadPool(30))
      .build();

  void writeIfModified(Gate gate) {
    if (!gate.isDirty()) { return; }
    int persistentHealth = gate.health.get();
    writeDb(gate);
    gate.writeScheduled = false;
    gate.persistentHealth.set(persistentHealth);
  }

  long writeBehindDelayMillis = 500;
  int changePercentage = 10;

  public void decreaseHealth(Id id, int damage) {
    Gate gate = cache.get(id);
    int persistentHealth = gate.persistentHealth.get();
    int newHealth = gate.health.addAndGet(-damage);
    if (!gate.writeScheduled) {
      int percentage = persistentHealth * 10 / 100;
      if (newHealth > persistentHealth + percentage ||
        newHealth < persistentHealth - percentage) {
        cache.invoke(id, entry -> {
          entry.setValue(entry.getValue());
          entry.setExpiryTime(entry.getStartTime() + writeBehindDelayMillis);
          entry.getValue().writeScheduled = true;
          return null;
        });
      }
    }
  }

这将在读取模式下操作缓存,因此cache.get()将触发初始的数据库加载。此外,如果需要的话,它使用到期和提前刷新来安排延迟写入。这有点棘手,因为通常和“文档化”的预刷新用例是不同的。如果有兴趣的话,我可以在博客文章中解释详细的机制。对于堆栈溢出的答案来说,它变得太重了。

当然,您也可以在其他缓存实现中使用其中的一些思想。

最后要注意的一点是:在用于原子化的案例地图计算中,如果并发同步数据库写入正在进行,则操作可能会阻塞。要么进行异步写入,要么使用不同的锁定来进行更新。

通过JPA缓存与写回方法进行性能比较仍然很有趣。

票数 1
EN

Stack Overflow用户

发布于 2021-10-24 18:05:43

是的,缓存是必需的。发出SQL请求太长了。就像它保存在RAM中一样,它显然更快。

你怎么能救他们?

  • 只需使用Map。

您必须根据您的数据类型,特别是键,选择地图的类型。

您可以获得更多关于它的信息,这里或这里。

  • 使用缓存管理器,如explain 这里有MapMaker (来自番石榴)
  • 您可以创建自己的缓存管理器。有时特别清洁,像这样:
代码语言:javascript
复制
public HashMap<UUID, Data> datas = new HashMap<>(); // here you have to choose the good map type

public void onEnable() {
   getServer().getScheduler().runTaskTimerAsynchronously(this, () -> {
      synchronized(datas) { // prevent used from others thread
         datas.values().forEach(Data::save); // save all data
         datas.clear();
      }
   }, 20 * 60, 20 * 60); // in ticks, 20 ticks = 1s so 20*60 = 1min
}

在这里,您保存所有对象并清除它们的每分钟。清除是为了防止过多的数据。

我建议你将多种东西结合起来:使用传道器,并且当它在很长时间内不被使用的时候保存下来。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/69698948

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档