我想知道是否已经有一个图书馆可以做到这一点,或者可能建议以哪种方式解决以下问题:
客户端A请求资源A,这是一个长时间运行的请求,因为资源A很昂贵,导致缓存未命中。在此期间,客户端B请求资源A,现在它仍然是高速缓存未命中,因为客户端A的请求尚未返回并填充高速缓存。因此,客户端B应该阻止并在客户端A的请求完成并填充缓存时得到通知,而不是发出新的请求来生成资源A.
我认为群组缓存库有这样的东西,但我还没有能够浏览代码来弄清楚他们是如何做到的,我也不想将实现与它结合起来并将其用作依赖。
到目前为止我唯一的解决方案是pub-sub类型的东西,我们有一个全局的当前飞行请求地图,其中reqID为关键。当req1到来时,它在地图中设置它的ID,req2来了并检查它的id是否在地图中,因为它请求相同的资源,所以我们阻塞了通知器通道。当req1完成时它会做3件事:
由于go并没有内置对广播的支持,因此可能会收听广播频道,然后保留每个请求广播的订阅者列表,或者我们可能会将地图更改为reqId =&gt ;列表(broadcastChannelSubscribers)。这些方面的东西。
如果您认为使用Go的原语有更好的方法,那么任何输入都会受到赞赏。困扰我的唯一一个解决方案是这个全球地图,被锁包围,我认为它很快就会成为瓶颈。如果你有一些非锁定的想法,即使它们是概率性的,我很高兴听到它们。
答案 0 :(得分:2)
这让我想起了一个人正在实施类似事情的问题:
我给出了一个实现这样一个中间层的例子。我认为这符合您的想法:定期跟踪对同一资源的请求,并防止它们被并行重新计算。
如果您有一个单独的例程负责接收请求和管理对缓存的访问,那么您不需要显式锁定(虽然有一个隐藏在通道中)。无论如何,我不知道你的应用程序的细节,但考虑到你需要检查缓存(可能已锁定)和(偶尔)执行一个昂贵的计算缺失条目 - 锁定地图查找似乎不是一个巨大的问题给我。如果您认为这会有所帮助,您也可以总是跨越更多这样的中间层例程,但是您需要一种确定性的方式来路由请求(因此每个缓存条目都由一个例程管理)。
很抱歉没有为您提供一个银弹解决方案,但听起来您无论如何都能解决您的问题。
答案 1 :(得分: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
}
}
这里我们只提取一个资源,但您可以为要获取的每个资源启动一个goroutine。 Goroutines很便宜,所以除非你需要数百万资源同时缓存,否则你应该没问题。你当然也可以在一段时间后杀死你的goroutines。
要跟踪哪个资源ID属于哪个频道,我会使用地图
map[resourceId]chan request
使用互斥锁。同样,如果获取资源是瓶颈,那么锁定地图的成本应该可以忽略不计。如果锁定地图是个问题,请考虑使用sharded map。
总的来说,你似乎很顺利。我建议尽可能简化设计,尽可能使用通道而不是锁。它们可以防止可怕的并发错误。
答案 2 :(得分:1)
一个解决方案是并发非阻塞缓存,详见The Go Programming Language第9章。
code samples非常值得一看,因为作者会带您浏览几个版本(memo1,memo2等),说明竞争条件的问题,使用互斥锁保护地图,以及仅使用频道的版本。< / p>
同时考虑https://blog.golang.org/context,因为它有类似的概念,并处理取消飞行请求。
将内容复制到此答案中是不切实际的,因此希望链接有用。
答案 3 :(得分:0)
Golang已将其作为功能func main() {
http.HandleFunc("/github", func(w http.ResponseWriter, r *http.Request) {
var key = "facebook"
var requestGroup singleflight.Group
// Search The Cache, if found in cache return from cache, else make single flight request
if res, err := searchCache(); err != nil{
return res
}
// Cache Miss-> Make Single Flight Request, and Cache it
v, err, shared := requestGroup.Do(key, func() (interface{}, error) {
// companyStatus() returns string, error, which statifies interface{}, error, so we can return the result directly.
if err != nil {
return interface{}, err
}
return companyStatus(), nil
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//Set the Cache Here
setCache(key, v)
status := v.(string)
log.Printf("/Company handler requst: status %q, shared result %t", status, shared)
fmt.Fprintf(w, "Company Status: %q", status)
})
http.ListenAndServe("127.0.0.1:8080", nil)
}
// companyStatus retrieves Comapny's API status
func getCompanyStatus() (string, error) {
log.Println("Making request to Some API")
defer log.Println("Request to Some API Complete")
time.Sleep(1 * time.Second)
resp, err := http.Get("Get URL")
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("Upstream response: %s", resp.Status)
}
r := struct{ Status string }{}
err = json.NewDecoder(resp.Body).Decode(&r)
return r.Status, err
}
提供。
对于您的用例,只需在单趟航班上使用一些额外的逻辑即可。考虑下面的代码片段:
<root>
<p id="111">5tw5t5et</p>
<p id="111">4qvtq3</p>
<p id="222">qv34tqv3</p>
<j>qv43tvq</j>
<p id="333">qv43tvq</p>
<p id="333">q34tvq43tvq</p>
<p id="333">q3434t3tvq43tvq</p>
</root>
我希望代码段能自我解释,您可以参考Single Flight Official Docs来深入研究单机飞行。