概述
singleflight.Group
是 Go 语言扩展包的另一种同步原语,它能够再一个服务中抑制对下游的多次重复请求。一个比较常见的使用场景是,我们使用 Redis 对数据库中的数据进行缓存,发生缓存击穿时,大量请求会打到数据库上进而影响服务的尾延时。
而 singleflight.Group
能够有效地解决这个问题,它能够限制对同一个键值对的多次重复请求,减少对下游的瞬时流量。
在资源的获取非常昂贵时(例如访问缓存、数据库),就很适合使用 singleflight.Group
优化服务。它的使用方法如下:
type service struct {
requestGroup singleflight.Group
}
func (s *service) handleRequest(ctx context.Context, request Request) (Response, error) {
v, err, _ := requestGroup.Do(request.Hash(), func() (interface{}, error) {
rows, err := // select * from tables
if err != nil {
return nil, err
}
return rows, nil
})
if err != nil {
return nil, err
}
return Response{
rows: rows,
}, nil
}
因为请求的哈希在业务上一般表示相同的请求,所以上述代码使用它作为请求的键。当然,我们也可以选择其他的字段作为 singleflight.Group.Do
方法的第一个参数减少重复的请求。
结构体
singleflight.Group
结构体由一个互斥锁 sync.Mutex
和一个映射表组成,每一个 singleflight.call
结构体都保存了当前调用对应的信息:
type Group struct {
mu sync.Mutex
m map[string]*call
}
type call struct {
wg sync.WaitGroup
val interface{}
err error
dups int
chans []chan 0
}
因为 val
和 err
两个字段都只会在 singleflight.Group.doCall
方法中赋值,所以当 singleflight.Group.doCall
和 sync.WaitGroup.Wait
返回时,函数调用的结果和错误都会返回给 singleflight.Group.Do
的调用方。
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
c.val, c.err = fn()
c.wg.Done()
g.mu.Lock()
delete(g.m, key)
for _, ch := range c.chans {
ch 0}
}
g.mu.Unlock()
}
- 行传入的函数
fn
,该函数的返回值会赋值给c.val
和c.err
- 调用
sync.WaitGroup.Done
方法通知所有等待结果的Goroutine
— 当前函数已经执行完成,可以从call
结构体中取出返回值并返回了 - 获取持有的互斥锁并通过管道将信息同步给使用
singleflight.Group.DoChan
方法的Goroutine
func (g *Group) DoChan(key string, fn func() (interface{}, error))