Java 使用MapMaker#makeComputingMap防止对相同数据同时进行RPC

Java 使用MapMaker#makeComputingMap防止对相同数据同时进行RPC,java,scala,concurrency,rpc,guava,Java,Scala,Concurrency,Rpc,Guava,我们有一个缓慢的后端服务器,它正在被负载压垮,我们希望中间层的Scala服务器对于每个唯一的查找只有一个未完成的后端请求 后端服务器仅存储不可变的数据,但在添加新数据时,中间层服务器将代表客户端请求最新数据,后端服务器很难处理负载。不可变数据使用写入时生成的唯一键缓存在memcached中,但是写入速率很高,因此我们得到的memcached命中率很低 我的一个想法是使用Google Guava的MapMaker#makeComputingMap()来包装实际的查找,在ConcurrentMap#

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

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

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

这似乎有点浪费,尽管代码很容易编写,请参阅下面的示例了解我的想法

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

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,为什么不简单地使用弱值或软值,让GC为您清理

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

我认为你的做法很合理。您只使用该结构来获取密钥上的锁条带,以确保对同一密钥的访问冲突。不用担心,您不需要每个键都有一个值映射
ConcurrentHashMap
和friends是Java libraries+Guava中唯一提供锁条带化的结构

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

如果你想让它尽可能便宜,你可以自己编写一些简单的锁条。基本上是一个包含N个锁(N=并发级别)的
对象[]
(或
数组[AnyRef]
:),您只需将查找键的散列映射到此数组中,然后锁定。这样做的另一个优点是,您实际上不必执行CHM所需的哈希代码技巧,因为后者必须将哈希代码分成一部分来选择锁,另一部分用于哈希表的需要,但您可以将整个哈希代码用于锁选择

编辑:在下面简述我的评论:

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
     }
   }
}
val并发级别=16
val locks=(对于(i)返回v
案例无=>
val valueFromBackend=backendServer.lookup(键)
cache.put(key,valueFromBackend)
返回值fromBackend
}
}
}

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

或1毫秒或类似的过期时间。Blair,开销是维护一个计时器,并为每个get创建一个TimerTask。实际上,它不会太大,但我会在做出决定之前进行分析。也就是说,对于这么小的超时时间,它似乎不是很有用。软/弱值不是更好吗?找到链接:Blair,我会如果这些额外的对象在您的系统中加起来会成为任何东西,因为这些东西在这么短的时间内仍然存在,这非常令人惊讶。您需要回答的真正问题是,您是否要重复向后端请求相同的值,而不重叠。当您显式删除时,您要保护的唯一东西实际上是并发的请求相同的值。串行请求将始终重新请求。与性能一样,只有在您实际看到问题时才进行优化,而不是在您认为可能存在问题时进行优化-您通常是错的:-)@Blair,这次讨论提醒我,我忘了在图表中包括计算映射,所以我添加了它,但只针对MapMaker的普通版本,因为事实证明,只有在这种情况下才有区别。如果您添加过期/逐出或特殊类型的键或值引用,则makeComputingMap()和makeMap()我在第三段中理解到,你只是建议锁定一个对象[]只是为了确保一次只有一个线程发出请求?我对返回结果感兴趣,这样阻塞线程就不必发出调用,大概在发出调用的N毫秒内,后端将返回相同的结果。嗯,对,我简化了一点。你检查缓存是否您想要的值已经存在,同时为密钥保留正确的锁?换句话说,如果线程锁定,并且在缓存中找不到该值(希望该部分是快速的),它会转到后端服务器本身,同时阻止所有其他需要相同密钥的线程。当另一个需要该密钥的线程获得锁时,首先检查它是否来得晚,另一个线程是否已经进行了调用。因此,第一个线程从后端服务器获取值,其余线程仅从缓存获取。我必须承认这一点当前使用过期计算映射的解决方案运行良好,无需太多额外的编码和调试,特别是因为我有一些不满意的用户。我完全同意,这是一个好的、简单的和可靠的解决方案。(尽管为什么要切换到过期?我想执行remove()立即就好。你是否也将MapMaker用作一种非常短期的缓存?以保护真正的缓存不受大量请求的影响?)好的。如果你在函数代码的开头输入检查真正的缓存是否已经得到了值,它会工作的,不是吗?我想你会犹豫是否再次检查缓存,因为缓存刚才没有值。避免代码在#remove()之后似乎不起作用在映射上调用。有关同时显示计算函数中多个线程的示例代码,请参阅。下一步将尝试使用过期和/或弱键。我使用MapMaker#expiration(1,TimeUnit.NANOSECONDS)使代码正常工作。我在计算中没有观察到多个线程