2015-06-27 58 views
1

我在想,如果已经有一个图书馆这样做,或者一个建议,去下面的问题,其办法:Golang检测飞行要求

客户端A使得对资源的请求,这是一个漫长运行请求,因为资源A很昂贵并且导致缓存未命中。与此同时,客户端B请求资源A,现在它仍然是缓存未命中,因为客户端A的请求尚未返回并填充缓存。所以客户端B应该阻塞并在客户端A的请求完成并且已经填充了缓存时通知客户端B,而不是发出新请求来生成资源A.

我认为组缓存库具有这些内容,但我无法浏览代码来弄清楚它们是如何实现的,我也不想将实现与它联系起来并使用它作为依赖。

我到目前为止唯一的解决方案是一个pub-sub类型的事物,我们有一个以reqID作为关键字的当前正在进行的请求的全局映射。当req1到达时,它在地图中设置它的ID,req2到来并检查它的id是否在地图中,因为它请求相同的资源,所以我们阻塞在通知器通道上。当REQ1完成它的三件事:

  1. 从地图
  2. 逐出其ID保存在高速缓存中
  3. 条目发送广播其ID到通知信道 REQ2接收到该通知,解除阻塞和从缓存中提取。

因为go没有内置的广播支持,所以可能有1个灌注监听广播频道,然后保留一个订户列表来广播每个请求,或者我们可以将地图更改为reqId = > list(broadcastChannelSubscribers)。沿着这些线的东西。

如果您认为有更好的方法来处理Go的基元,任何输入将不胜感激。唯一让我困扰的解决方案就是这张全球地图,被锁定环绕,我认为它很快会成为一个瓶颈。如果你有一些非锁定的想法,即使他们是概率性的,我很乐意听到他们的意见。

回答

2

它提醒如果有人正在实施类似的事情,一个问题我:

Coalescing items in channel

我给了一个答案实现这样一个中间层的一个例子。我认为这符合你的想法:定期跟踪对同一资源的请求并防止它们被重新并行地重新计算。

如果您有单独的例程负责接收请求并管理对缓存的访问,则不需要显式锁定(尽管存在一个锁定)。无论如何,我不知道你的应用程序的具体情况,但考虑到你需要检查缓存(可能是锁定的)和(偶尔)执行缺少进入的昂贵计算 - 锁定地图查找对我来说似乎不是一个大问题。如果您认为这会有所帮助,您总是可以跨越更多这样的中间层例程,但是您需要一种确定性的路由请求方式(因此每个缓存项由单个例程管理)。

对不起,不给你一个银色子弹解决方案,但它听起来像你是一个很好的方式来解决你的问题。

+0

似乎几乎相同,在我的情况下,因为请求是相当重的锁概率不会是瓶颈,我只是想知道是否有一个很好的锁免费的方式做到这一点 – Feras

2

缓存和性能问题总是很棘手,您应该始终提供一个基准解决方案,以确保您的假设是正确的。但是如果我们知道瓶颈在获取资源并且缓存将带来巨大回报,那么您可以使用Go的渠道来实施排队。假设response是您的资源的类型。

type request struct { 
    back chan *response 
} 

func main() { 
    c := make(chan request,10) // non-blocking 
    go func(input chan request){ 
     var cached *response 
     for _,i := range input { 
      if cached == nil { // only make request once 
       cached = makeLongRunningRequest() 
      } 
      i.back <- cached 
     } 
    }(c) 

    resp := make(chan *response) 

    c <- request{resp} // cache miss 
    c <- request{resp} // will get queued 
    c <- request{resp} // will get queued 

    for _,r := range resp { 
     // do something with response 
    } 
} 

这里我们只取一个资源,但你可以开始为要提取每个资源的一个够程。 Goroutines很便宜,除非你需要数百万资源同时缓存,否则你应该没问题。你当然也可以在一段时间后杀死你的goroutines。

为了保持其资源ID属于哪个频道,我会使用一个地图

map[resourceId]chan request 

与互斥轨道。同样,如果获取资源是瓶颈,那么锁定地图的成本应该可以忽略不计。如果锁定地图成为问题,请考虑使用sharded map

总的来说,你似乎很好。我建议尽量保持设计尽可能简单,并尽可能使用通道而不是锁。他们确实避免了可怕的并发错误。

+0

我认为这几乎是什么我建议,但是我认为你的方法是:'map [resourceId] chan request'将不起作用,因为每个resourceId你需要多个通道,因为你可以在同一个resourceId上有多个将来的请求挂起,你可以通过一个频道向多个用户广播,所以你需要一个频道,每个后续请求,更像是:'map [res_id] - > [slice [channels_to_notify]]' – Feras

+0

@Feras是我认为它会工作(但我没有'测试它)。 Go中的广播模式并不常见,这不是广播。 goroutine一次只知道一个请求。对于来自输入通道的每个“请求”,它在输出通道上放置一个“*响应”。拥有一个输入通道是一项功能,因为当您一次发送多个输入时,它充当线程安全队列。使其无缓冲会阻止执行,如你所愿。在一个真实的应用程序中,你可能需要不同的输出通道,你可以像这样得到'c < - request {chan1}','c < - request {chan2}'等 –

+0

作为奖励,当你关闭输入通道goroutine将会干净地退出并且缓存的*响应将被垃圾收集。如果您只想在有限的时间内将内容保存在缓存中,这很有用。所以大部分的复杂性都保存在goroutine中,这意味着多线程的东西污染你的程序的风险较小。 –

1

一种解决方案是在详细讨论The Go Programming Language并发非阻塞的高速缓存,第9章

code samples是非常值得一看,因为笔者带您穿越几个版本(memo1,memo2等),说明竞争条件的问题,使用互斥体保护地图,以及仅使用频道的版本。

也考虑https://blog.golang.org/context,因为它有类似的概念,并处理取消在飞行中的请求。

将内容复制到此答案是不切实际的,因此希望链接有用。