gocookbook
gocookbook copied to clipboard
Go并发编程--SingleFlight
SingleFlight
是Go语言sync
扩展库提供的另一种并发原语,那么SingleFlight
是用于解决什么问题的呢?官方文档里的解释是:
Package singleflight provides a duplicate function call suppression mechanism.
翻译过来就是:singleflight包提供了一种抑制重复函数调用的机制。
具体到Go
程序运行的层面来说,SingleFlight
的作用是在处理多个goroutine
同时调用同一个函数的时候,只让一个goroutine
去实际调用这个函数,等到这个goroutine
返回结果的时候,再把结果返回给其他几个同时调用了相同函数的goroutine
,这样可以减少并发调用的数量。在实际应用中也是,它能够在一个服务中减少对下游的并发重复请求。还有一个比较常见的使用场景是用来防止缓存击穿。
Go
扩展库里用singleflight.Group
结构体类型提供了SingleFlight
并发原语的功能。
singleflight.Group
类型提供了三个方法:
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
func (g *Group) Forget(key string)
-
Do方法,接受一个字符串Key和一个待调用的函数,会返回调用函数的结果和错误。使用Do方法的时候,它会根据提供的Key判断是否去真正调用
fn
函数。同一个 key,在同一时间只有第一次调用Do方法时才会去执行fn
函数,其他并发的请求会等待调用的执行结果。 -
DoChan方法:类似Do方法,只不过是一个异步调用。它会返回一个通道,等
fn
函数执行完,产生了结果以后,就能从这个 chan 中接收这个结果。 -
Forget方法:在SingleFlight中删除一个Key。这样一来,之后这个Key的Do方法调用会执行
fn
函数,而不是等待前一个未完成的fn
函数的结果。
使用缓存时,一个常见的用法是查询一个数据先去查询缓存,如果没有就去数据库里查到数据并缓存到Redis
里。缓存击穿问题是指,高并发的系统中,大量的请求同时查询一个缓存Key 时,如果这个 Key 正好过期失效,就会导致大量的请求都打到数据库上,这就是缓存击穿。用 SingleFlight
来解决缓存击穿问题再合适不过,这个时候只要这些对同一个 Key 的并发请求的其中一个到数据库中查询就可以了,这些并发的请求可以共享同一个结果。用 SingleFlight
能够限制对同一个缓存 Key 的多次重复请求,减少对下游的瞬时流量。
下面是一个模拟用
SingleFlight
并发原语合并查询Redis
缓存的程序,你可以自己动手测试一下,开10个goroutine
去查询一个固定的Key,观察一下返回结果就会发现最终只执行了一次Redis
查询。
// 模拟一个Redis客户端
type client struct {
// ... 其他的配置省略
requestGroup singleflight.Group
}
// 普通查询
func (c *client) Get(key string) (interface{}, error) {
fmt.Println("Querying Database")
time.Sleep(time.Second)
v := "Content of key" + key
return v, nil
}
// SingleFlight查询
func (c *client) SingleFlightGet(key string) (interface{}, error) {
v, err, _ := c.requestGroup.Do(key, func() (interface{}, error) {
return c.Get(key)
})
if err != nil {
return nil, err
}
return v, err
}
完整可运行的示例代码,访问:https://github.com/kevinyan815/gocookbook/tree/master/codes/singleflight 。