我正在为斯卡拉期货设置一个http请求范围的缓存。作为单个请求的一部分,有多个昂贵的操作(REST API调用),其结果将在响应中返回。对同一资源可能有重复的REST调用。为了避免重复,我正在设置一个请求范围缓存,以存储这些REST调用返回的期货。因此,下一次在该单个请求中尝试进行相同的REST调用时,结果将从缓存中加载。
选项1: 我在scala 2.11中使用了play框架2.6。我尝试了AsyncCacheApi :: getOrElseUpdate方法。 但是根据文档: https://www.playframework.com/documentation/2.6.x/ScalaCache#Accessing-the-Cache-API 注意:getOrElseUpdate不是Ehcache中的原子操作,它实现为get,随后先计算值,再计算set。这意味着,如果多个线程同时调用getOrElse,则有可能多次计算该值。
选项2: 我从以下线程尝试了akka-http-cache: https://groups.google.com/forum/#!topic/play-framework/qVDMvqZidpI 即使失败,Play也会缓存您的将来,因此,即使您的api调用失败,缓存中的将来也会是例外。 Spray-Cache仅缓存成功的期货并解决雷电群。如果您只希望本地缓存而不是共享的分布式缓存,则可以很好地进行设置。不要尝试将其连接到播放缓存框架,请按照Spray-Cache文档中的说明使用。
https://doc.akka.io/docs/akka-http/current/common/caching.html 缓存API的中心思想是不将类型T的实际值本身存储在缓存中,而是存储相应的期货,即类型Future [T]的实例。这种方法的优点是可以解决雷电群问题,在这种情况下,对特定缓存密钥(例如资源URI)的许多请求会在第一个请求完成之前到达。通常(在没有特殊保护技术的情况下,例如所谓的“牛仔”条目),这可能导致许多请求在试图计算相同结果的同时竞争系统资源,从而大大降低了整体系统性能。当您使用Akka HTTP缓存时,针对某个缓存密钥的第一个请求将导致将来放入缓存,所有以后的请求随后都将“挂钩”。第一个请求完成后,所有其他请求也将完成。这样可以最大程度地减少所有请求的处理时间和服务器负载。
基于上述选项2的示例代码:
import akka.http.caching.LfuCache
import akka.http.caching.scaladsl.Cache
case class ChildResource(...)
case class MyResource1(id: String, attrs: JsValue, childResource: Future[ChildResource])
case class MyResource2(id: String, attrs: JsValue, childResource: Future[ChildResource])
...
case class MyResourceN(id: String, attrs: JsValue, childResource: Future[ChildResource])
object MyCache {
val childResourceCache = : Cache[String, ChildResource] = LfuCache[String, ChildResource]
val resource1Cache = : Cache[String, MyResource1] = LfuCache[String, MyResource1]
val resource1Cache = : Cache[String, MyResource1] = LfuCache[String, MyResource1]
...
val resourceNCache = : Cache[String, MyResourceN] = LfuCache[String, MyResourceN]
}
@Singleton
class RestClient(wsClient: WSClient) {
def getChildResource(requestId: String, id: String): Future[ChildResource] = MyCache.childResourceCache.getOrLoad(requestId + id, _ => wsClient.url(...))
def getResource1(requestId: String, id: String): Future[MyResource1] = {
MyCache.resource1Cache.getOrLoad(requestId + id, _ => {
val responseFuture = wsClient.url(...)
...
val childResourceFuture = getChildResource(...)
...
})
}
def getResource2(requestId: String, id: String): Future[MyResource2] = {
MyCache.resource2Cache.getOrLoad(requestId + id, _ => {
val responseFuture = wsClient.url(...)
...
val childResourceFuture = getChildResource(...)
...
})
}
...
def getResourceN(requestId: String, id: String): Future[MyResourceN] = {
MyCache.resourceNCache.getOrLoad(requestId + id, _ => {
val responseFuture = wsClient.url(...)
...
val childResourceFuture = getChildResource(...)
...
})
}
}
case class MyResponse(
myResource1: Future[MyResource1],
myResource2: Future[MyResource2],
...
myResourceN: Future[MyResourceN]
)
object MyResponse {
def apply(...): MyResponse = {
// multiple parallel threads make "getResource" REST calls to fetch all resources
...
}
}
@Singleton
@Api(value = "/myresource")
class MyController @Inject()
(implicit val executionContext: ExecutionContext, implicit val materializer: Materializer) {
def myMethod(...) = {
val requestId = UUIDs.timeBased().toString
val myResponse = MyResponse(...)
// lets remove the items cached for this request as we're ready to return the response
val childResourceCacheKeys = MyCache.childResourceCache.keys.filter(_.startsWith(requestId))
childResourceCacheKeys.foreach(MyCache.childResourceCache.remove(_))
val resource1CacheKeys = MyCache.resource1Cache.keys.filter(_.startsWith(requestId))
resource1CacheKeys.foreach(MyCache.resource1Cache.remove(_))
val resource2CacheKeys = MyCache.resource2Cache.keys.filter(_.startsWith(requestId))
resource2CacheKeys.foreach(MyCache.resource2Cache.remove(_))
...
val resourceNCacheKeys = MyCache.resourceNCache.keys.filter(_.startsWith(requestId))
resourceNCacheKeys.foreach(MyCache.resourceNCache.remove(_))
// return response
myResponse
}
}
唯一的低效率部分是在该请求生命周期结束时手动清除与给定请求有关的缓存项。 是否有任何库/替代品可以为scala期货提供现成的此类HTTP请求范围缓存?因此,每次请求后,缓存的项目将自动用于GC ...