kevinyan815 / gocookbook

go cook book
MIT License
789 stars 167 forks source link

Go并发编程--SingleFlight #31

Open kevinyan815 opened 3 years ago

kevinyan815 commented 3 years ago

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)

使用缓存时,一个常见的用法是查询一个数据先去查询缓存,如果没有就去数据库里查到数据并缓存到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