Http Golang检测飞行中的请求
我想知道是否已经有一个图书馆可以做到这一点,或者可能有一个解决以下问题的建议: 客户端A请求资源A,这是一个长时间运行的请求,因为资源A很昂贵,并且会导致缓存丢失。同时客户端B请求资源A,现在仍然是缓存未命中,因为客户端A的请求尚未返回并填充缓存。因此,当客户端a的请求完成并填充了缓存时,客户端B应该阻塞并得到通知,而不是发出新的请求来生成资源a 我认为组缓存库具有类似的功能,但我无法浏览代码以了解它们是如何实现的,我也不想将实现与之绑定,并将其用作依赖项 到目前为止,我唯一的解决方案是发布子类型的东西,其中我们有一个当前飞行中请求的全局映射,其中reqID是一个键。当req1出现时,它在映射中设置其ID,req2出现并检查其ID是否在映射中,因为它请求的是与它相同的资源,所以我们在通知程序通道上阻塞。当req1完成时,它会做3件事:Http Golang检测飞行中的请求,http,caching,concurrency,go,request,Http,Caching,Concurrency,Go,Request,我想知道是否已经有一个图书馆可以做到这一点,或者可能有一个解决以下问题的建议: 客户端A请求资源A,这是一个长时间运行的请求,因为资源A很昂贵,并且会导致缓存丢失。同时客户端B请求资源A,现在仍然是缓存未命中,因为客户端A的请求尚未返回并填充缓存。因此,当客户端a的请求完成并填充了缓存时,客户端B应该阻塞并得到通知,而不是发出新的请求来生成资源a 我认为组缓存库具有类似的功能,但我无法浏览代码以了解它们是如何实现的,我也不想将实现与之绑定,并将其用作依赖项 到目前为止,我唯一的解决方案是发布子类
如果您认为使用Go的原语有更好的方法,任何输入都将不胜感激。这个解决方案中唯一困扰我的是这个全局地图,它被锁包围着,我想它很快就会成为一个瓶颈。如果你有一些非锁定的想法,即使它们是随机的,我也很高兴听到它们。这让我想起一个问题,有人在实施类似的事情: 我给出了一个实现这种中间层的示例。我认为这与您的想法是一致的:有一个例行程序来跟踪对相同资源的请求,并防止它们并行重新计算 如果您有一个单独的例程负责接收请求和管理对缓存的访问,那么您不需要显式锁(尽管在通道中隐藏了一个锁)。无论如何,我不知道您的应用程序的具体情况,但考虑到您需要检查缓存(可能是锁定的)和(偶尔)对丢失的条目执行昂贵的计算,地图锁定查找对我来说似乎不是一个大问题。如果您认为这会有所帮助,那么您也可以跨越更多这样的中间层例程,但是您需要一种确定的请求路由方式(因此每个缓存条目都由一个例程管理)
很抱歉没有为您提供银弹解决方案,但听起来您已经找到了解决问题的好方法。缓存和性能问题总是很棘手,您应该始终制定一个基本的解决方案来进行基准测试,以确保您的假设是正确的。但是,如果我们知道瓶颈是获取资源,并且缓存将提供显著的回报,那么您可以使用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
}
}
使用互斥锁。同样,如果获取资源是瓶颈,那么锁定地图的成本应该可以忽略不计。如果锁定地图是一个问题,请考虑使用.< /P>
总的来说,你似乎进展顺利。我建议尽量让你的设计尽可能简单,如果可能的话,用通道代替锁。它们确实可以防止严重的并发错误。一种解决方案是并发非阻塞缓存,如第9章的详细讨论 这两个版本非常值得一看,因为作者带您浏览了几个版本(memo1、memo2等),说明了竞争条件的问题,使用互斥来保护地图,以及一个只使用通道的版本
也考虑到它有类似的概念,并处理取消飞行请求。
将内容复制到这个答案中是不切实际的,所以希望这些链接是有用的。这已经由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
}
我希望代码片段是不言自明的,您可以参考以深入研究单次飞行。我认为这正是我的建议,但是我认为您的方法:
map[resourceId]chan request
不起作用,因为每个resourceId需要多个通道,由于您可以在同一个resourceId上挂起多个未来请求,并且您不能通过一个频道向多个订户广播,因此每个后续请求都需要一个频道,更像:map[res\u id]->[slice[channels\u to\u notify]
@Feras是的,我认为它可以工作(但我还没有测试它)。这种广播模式在围棋中并不常见,这不是广播。goroutine一次只知道一个请求。对于来自输入通道的每个请求
,它在输出通道上放置一个*响应
。拥有一个输入通道是一项功能,因为当您同时发送多个输入时,它充当线程安全队列。使其无缓冲将阻止您想要的执行。在实际的应用程序中,您可能需要不同的输出通道,您可以像这样获得c,作为奖励,当您关闭输入通道时,goroutine将干净地退出,缓存的*响应将被垃圾收集。如果您只想在缓存中保留有限的时间,则此功能非常有用。所以大部分复杂性都保存在t中
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
}