针对场景
SingleFlight用来合并请求,减轻对下游服务的压力
SingleFlight的作用是,多个goroutine调用同一个方法时,只让一个goroutine去调,其他goroutine等待,当调用的goroutine完成调用后,会将结果返回给其他等待的goroutine
使用SingleFlight,在面对高并发请求的场景时,而且这些请求都是读请求,可以将多个请求合并成一个请求,减轻后端服务的压力,对于像数据库这样的后端服务性能较低,使用SingleFlight就可以极大提升性能
SingleFlight常用于解决 缓存击穿的问题
实现原理
type Group struct {
mu sync.Mutex // 保护m的并发安全
m map[string]*call // 保存请求的key、请求的调用对象
}
// 请求调用对象
type call struct {
wg sync.WaitGroup // 用来阻塞没有执行的goroutine
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val interface{} // 请求执行完后的值
err error // 请求执行完后的error值
forgotten bool // 是否忘掉这个key
dups int // 执行这个key的goroutine数量
chans []chan<- Result
}
type Result struct {
Val interface{}
Err error
Shared bool
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
// 初始化map
if g.m == nil {
g.m = make(map[string]*call)
}
// key存在,说明有goroutine在处理请求,dups++后调用wg.wait阻塞等待;唤醒后,返回保存在call对象中的val、err
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
// key不存在,创建call对象,调用wg.Add阻塞并发的goroutine,将key保存到map中证明有人在处理请求
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
......
defer func() {
// 唤醒其他等待的goroutine
c.wg.Done()
g.mu.Lock()
defer g.mu.Unlock()
// 如果forgotten为false,那么会删除请求的key,即该次请求结束后,下一次请求该key时会再执行一次fn方法
if !c.forgotten {
delete(g.m, key)
}
if e, ok := c.err.(*panicError); ok {
// In order to prevent the waiting channels from being blocked forever,
// needs to ensure that this panic cannot be recovered.
if len(c.chans) > 0 {
go panic(e)
select {} // Keep this goroutine around so that it will appear in the crash dump.
} else {
panic(e)
}
} else if c.err == errGoexit {
// Already in the process of goexit, no need to call again
} else {
// Normal return
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
}
}()
func() {
defer func() {
if !normalReturn {
// Ideally, we would wait to take a stack trace until we've determined
// whether this is a panic or a runtime.Goexit.
//
// Unfortunately, the only way we can distinguish the two is to see
// whether the recover stopped the goroutine from terminating, and by
// the time we know that, the part of the stack trace relevant to the
// panic has been discarded.
if r := recover(); r != nil {
c.err = newPanicError(r)
}
}
}()
// 调用fn方法,保存value、err到call对象
c.val, c.err = fn()
normalReturn = true
}()
......
}
实践
https://github.com/golang/groupcache
https://github.com/cockroachdb/cockroach
https://github.com/coredns/coredns
这些项目中都有用到SingleFlight