【问题标题】:Scala [2.11.6] -- is there an elegant way to create cache-keys for objects based on an Int/Long and the full class name?Scala [2.11.6]——有没有一种优雅的方法可以基于 Int/Long 和完整的类名为对象创建缓存键?
【发布时间】:2015-08-27 14:25:36
【问题描述】:

我想使用缓存来保存刚刚来自数据库读取的最近访问的对象。

在我的例子中,数据库主键是 Long。

在每种情况下,我都会有一个表示该数据的对象(案例类)。

Long 加上完整的类名的组合将成为查找任何特定对象的唯一标识符。 (命名空间不应该有冲突,因为类名不使用数字(通常?)。无论如何,对于这个用例,我控制整个命名空间,所以不是一个大问题)。

对象在缓存中的寿命相对较短——我只看到了一些情况,我可以通过多次保存相同的不可变对象来节省内存,而不是同一对象的不同实例,这将非常难以“到处传递一切”以避免。

这也有助于在不同眼球检查相同内容的情况下提高性能,但这不是这个特定用例的驱动因素(只是肉汁)。

我现在担心 时间我需要一个给定的对象,我需要重新创建缓存键。这将涉及 Long.toString 和 String Concat。有问题的案例类在它们的伴生对象中有一个 val ,这样它们就知道它们的类名而不会发生任何进一步的反射。

我正在考虑将“缓存”放在主缓存键的伴随对象中,因为我希望避免(不必要的?)每次查找的重复操作以及由此产生的垃圾收集等。(最快的代码要运行的是永远不会被编写(或调用)的代码 - 对吧?)

有没有更优雅的方法来处理这个问题?其他人已经解决了这个特定问题吗?

我曾想过编写一个键类,但即使对 hash 和 toString 使用 val(惰性或其他方式),我仍然会为我要求的每个对象获得命中,因为现在我必须每次都创建键对象。 (这当然可以回到伴随对象的键缓存中,但如果我为键设置伴随对象缓存的麻烦,那么键对象方法是多余的。)

作为这个问题的第二个问题 - 假设我使用 Long 和完整的类名(作为字符串)最有可能获得最快的缓存拉取?

Long.toString + fullClassName

fullClassName + Long.toString

Long 是键中的字符串,所以假设它是缓存中的字符串“find”,这样更容易索引查找?首先是数字部分或字符串类名。

数字优先意味着您遍历所有具有匹配数字的对象以搜索匹配的类,而类优先意味着您首先找到特定类的块,但您必须走到字符串的最后才能找到完全匹配.

我怀疑前者可能更容易针对“快速查找”进行优化(我知道用 MySQL 术语来说是......)

那么,也许有人已经有了基于双键查找的缓存? :)

【问题讨论】:

  • 将存在多少个应用程序实例?如果不止一个,你是假设一个分布式缓存还是每个应用实例都有自己的缓存?
  • 好问题 - 没有试图假设任何东西,但我怀疑每个应用程序都有自己的缓存。处理深度嵌套的分层对象只是短暂的,这些对象往往会不时追逐自己的尾巴。

标签: scala caching


【解决方案1】:

在您获得相反的具体性能指标之前,我会保持非常简单。比如:

trait Key {

  def id: Long

  lazy val key: String = s"${getClass.getName}-${id}"

}

case class MyRecordObject(id: Long, ...) extends Key

使用简单的现有缓存解决方案,例如Guava Caching

对于您的第二个问题,在您真正证明密钥生成是一个瓶颈(我有点怀疑它是否会成为瓶颈)之前,我根本不会担心生成密钥的性能。

【讨论】:

  • 不担心密钥生成的成本 - 我担心之后垃圾收集的成本和/或内存成本,因为成千上万的这些密钥在 GC 之前一直浮动。
  • @Techmag,您最好通过 JVM 调优解决 GC 问题。
  • 如果我知道答案,我就不会问这个问题了 :) 如果可以避免一开始就造成混乱,为什么还要尝试解决问题?
  • 如果您认为创建许多短期对象是一团糟,那么我不知道 Scala(或一般的 JVM)是​​正确的选择。
  • 他们说最好的编写代码是永远不会运行的代码。换句话说,效率永远是口头禅。我根本不反对许多短命的对象——我只是使用历史上在数以万计的对象中拥有数据集的页面(详细的系统概述)。我要做的就是避免不必要创建许多短期对象。每个项目都平衡一组标准与另一组标准,在这种情况下,由于该项目的数据集和所需显示的复杂性,我想节省内存并为 CG 节省一些精力。
【解决方案2】:
import play.api.cache.Cache

事实证明,Cache.getOrElse[T](idAsString, seconds) 实际上完成了大部分繁重的工作!

[T] 在 Scala 中当然是 type ,这足以将缓存中的内容分开。每个 [T] 是缓存中唯一的、独立的和不同的存储桶。

所以Cache.getOrElse[AUser](10, 5) 将得到一个与Cache.getOrElse[ALog](10, 5) 完全不同的对象(这里为了说明的目的,ID 10 恰好是相同的)。

我目前正在对数百种类型的数千个对象执行此操作,所以我知道它有效...

我说 Long 的大部分工作都必须经过.toString'ed 才能用作密钥。不是一个完整的 GC 灾难,因为我只是设置了一个 Map 来保存最常用/最近的 .toString'ed Long 值。

对于那些根本不明白这一点的人来说,可以考虑一个简单的日志屏幕,这在大多数 Web 应用程序中都很常见。

2015/10/22 10:22 - Johnny Rotten - deleted an important file
2015/10/22 10:22 - Johnny Rotten - deleted another important file
2015/10/22 10:22 - Johnny Rotten - looked up another user
2015/10/22 10:22 - Johnny Rotten - added a bogus file
2015/10/22 10:22 - Johnny Rotten - insulted his boss

在 Java (Tomcat) 下,通常会有一个代表该用户 (Johnny Rotten) 的 single 对象,并且该 single 对象每次都会链接到名称该用户出现在日志显示中。

现在在 Scala 下,我们倾向于为日志条目的每一行创建一个新实例(Case Class),这仅仅是因为我们没有(有效/管道)方法来获取最后使用的实例 该案例类。日志本身往往是一个案例类,它有一个用户案例类的lazy val

因此,出现了 user-x,他们查找日志并将分页设置为 500 行和低,现在我们创建了 500 个案例类,只是为了显示用户名(每个日志中的“谁”入口)。

然后几秒钟后,我们又有 500 个 User 案例类点击刷新,因为他们认为他们没有第一次点击鼠标...

但是,如果使用一个简单的缓存来保存一个最近访问的对象(例如 5 秒),我们为整个 500 个日志条目创建的只是一个 User 案例类的单个实例,用于我们在日志中显示的每个唯一名称.

在 Scala 中,类是不可变的,因此单个实​​例在这里是完全可以接受的用例,GC 没有不必要的工作要做......

【讨论】:

    猜你喜欢
    • 2014-08-06
    • 2021-08-14
    • 2023-01-20
    • 2010-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-07
    • 1970-01-01
    相关资源
    最近更新 更多