【问题标题】:Using MapMaker#makeComputingMap to prevent simultaneous RPCs for the same data使用 MapMaker#makeComputingMap 防止同一数据的同时 RPC
【发布时间】:2010-10-28 01:08:27
【问题描述】:

我们有一个缓慢的后端服务器,它被负载压垮了,我们希望中间层的 Scala 服务器对于每个独特的查找只有一个未完成的后端请求。

后端服务器只存储不可变的数据,但在添加新数据时,中间层服务器将代表客户端请求最新数据,后端服务器难以承受负载。不可变数据使用写入时生成的唯一键缓存在 memcached 中,但写入速率很高,因此我们得到的 memcached 命中率较低。

我的一个想法是使用 Google Guava 的 MapMaker#makeComputingMap() 来包装实际查找,在 ConcurrentMap#get() 返回后,中间层将保存结果并从 Map 中删除键。

这似乎有点浪费,虽然代码很容易写,看下面的例子我在想什么。

是否有更自然的数据结构、库或 Guava 的一部分可以解决这个问题?

import com.google.common.collect.MapMaker

object Test
{
  val computer: com.google.common.base.Function[Int,Long] =
  {
    new com.google.common.base.Function[Int,Long] {
      override
      def apply(i: Int): Long =
      {
        val l = System.currentTimeMillis + i
        System.err.println("For " + i + " returning " + l)
        Thread.sleep(2000)
        l
      }
    }
  }

  val map =
  {
    new MapMaker().makeComputingMap[Int,Long](computer)
  }

  def get(k: Int): Long =
  {
    val l = map.get(k)
    map.remove(k)
    l
  }

  def main(args: Array[String]): Unit =
  {
    val t1 = new Thread() {
      override def run(): Unit =
      {
        System.err.println(get(123))
      }
    }

    val t2 = new Thread() {
      override def run(): Unit =
      {
        System.err.println(get(123))
      }
    }

    t1.start()
    t2.start()
    t1.join()
    t2.join()

    System.err.println(get(123))
  }
}

【问题讨论】:

  • 在地图上调用#remove() 后,避免代码似乎不起作用。请参阅code.google.com/p/guava-libraries/issues/detail?id=462 获取同时显示计算函数中的多个线程的示例代码。接下来将尝试过期和/或弱密钥。
  • 我使用 MapMaker#expiration(1, TimeUnit.NANOSECONDS) 让代码工作。我一次在计算函数中观察到的线程不超过一个。

标签: java scala concurrency rpc guava


【解决方案1】:

我不确定你为什么要自己实现 remove,为什么不简单地设置弱或软值并让 GC 为你清理?

new MapMaker().weakValues().makeComputingMap[Int, Long](computer)

【讨论】:

  • 或 1 ms 之类的过期时间。
  • Blair,开销是维护一个 Timer 并为每个 get 创建一个 TimerTask。在实践中它不会那么大,但我会在做出决定之前进行分析。也就是说,对于这么小的超时,它似乎不是很有用。软/弱值不是更好吗?
  • Blair,如果这些额外的对象会在你的系统中添加到任何东西上,我会感到非常惊讶,因为它们的生命周期很短。您需要回答的真正问题是您是否要在不重叠的情况下重复向后端询问相同的值。当您显式删除时,您唯一要防止的实际上是对相同值的并发请求。串行请求将总是重新请求。与性能一样,只有当你真正看到问题时才进行优化,而不是在认为可能存在问题时进行优化——你通常是错的 :-)
  • @Blair,这个讨论提醒我,我忘了在该图表中包含计算地图,所以我添加了这一点,但仅适用于 MapMaker 的普通版本,因为事实证明,只有在那种情况下是一个区别。如果您为键或值添加过期/驱逐或特殊类型的引用,则 makeComputingMap() 和 makeMap() 具有相同的成本。
【解决方案2】:

我觉得你做的很合理。您只使用该结构来获取密钥上的锁条,以确保对相同密钥的访问发生冲突。不用担心您不需要每个键的值映射。 ConcurrentHashMap 和朋友是 Java 库+Guava 中唯一提供锁条的结构。

这确实会导致一些较小的运行时开销,加上您不需要的哈希表的大小(如果对同一段的访问堆积起来并且 remove() 跟不上,它甚至可能会增长)。

如果你想让它尽可能便宜,你可以自己编写一些简单的锁条。基本上是 N 个锁(N = 并发级别)的Object[](或Array[AnyRef] :)),您只需将查找键的哈希映射到该数组中,然后锁定。这样做的另一个好处是你真的不必做 CHM 需要做的 hashcode 技巧,因为后者必须将 hashcode 分成一个部分来选择锁,另一个来满足 hashtable 的需要,但是你可以将其全部用于锁定选择。

编辑:在下面草绘我的评论:

val concurrencyLevel = 16
val locks = (for (i <- 0 to concurrencyLevel) yield new AnyRef).toArray

def access(key: K): V = {
   val lock = locks(key.hashCode % locks.size)
   lock synchronized {
     val valueFromCache = cache.lookup(key)
     valueFromCache match {
       case Some(v) => return v
       case None =>
         val valueFromBackend = backendServer.lookup(key)
         cache.put(key, valueFromBackend)
         return valueFromBackend
     }
   }
}

(顺便说一句,需要toArray 调用吗?或者返回的IndexSeq 已经可以通过索引快速访问?)

【讨论】:

  • 正如我在第三段中所理解的那样,您是否只是建议锁定一个 Object[] 只是为了确保一次只有一个线程发出请求?我有兴趣返回结果,以便阻塞线程不必进行调用,大概在 N 毫秒内进行调用,后端将返回相同的结果。
  • 嗯,对,我有点过于简单化了。您如何检查缓存是否您想要的值已经存在同时为键保持正确的锁定?换句话说,如果一个线程被锁定,并且在缓存中没有找到值(希望那部分很快),它会进入后端服务器本身,同时阻塞所有其他想要相同密钥的线程。当另一个想要该密钥的线程获取锁时,首先检查它是否迟到并且另一个线程已经进行了调用。所以第一个线程从后端服务器获取值,其余的只从缓存中获取。
  • 我必须承认,当前使用过期计算图的解决方案运行良好,无需额外的编码和调试,特别是因为我有不满意的用户。
  • 完全同意,这是一个很好、简单且可靠的解决方案。 (虽然你为什么切换到过期?我认为立即执行 remove() 更好。你是否也将 MapMaker 用作一种非常短期的缓存?保护真正的缓存免受大量请求的影响?)
  • 对。好吧,如果你把你的函数代码放在开始检查真正的缓存是否已经得到了值,它会工作,不是吗?我猜你会犹豫是否再次检查缓存,因为你这样做的真正原因是因为缓存在不久前没有值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-10-31
  • 2014-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-18
  • 1970-01-01
相关资源
最近更新 更多