开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 24 天,点击查看活动详情
SingleFlight
SingleFlight 是 Go 语言 sync 包提供的一个功能,用于避免重复请求并发执行,同时还能减轻后端的压力。
SingleFlight 的主要思想是,在进行并发请求时,只执行一次请求,其他的请求则等待这个请求返回结果,然后重用结果。这就相当于将一些重复的请求合并为一次请求,可以减轻后端服务器的压力,同时也避免了一些竞态条件。
SingleFlight 主要包含以下三个方法:
- Do:执行请求,并返回结果;对于同一个 key,在同一时间只有一个在执行,同一个 key 并发的请求会等待。第一个执行的请求返回的结果,就是它的返回结果。
- DoChan:类似 Do 方法,只不过是返回一个 chan,等 fn 函数执行完,产生了结果以后,就能从这个 chan 中接收这个结果。
- Forget:告诉 Group 忘记这个 key。这样一来,之后这个 key 请求会执行 f,而不是等待前一个未完成的 fn 函数的结果。
SingleFlight 的实现过程主要是利用了 Go 语言中的 sync.Mutex 和 sync.Cond,使用互斥锁和条件变量进行并发控制,保证请求的唯一性和并发安全性。
下面是一个简单的 SingleFlight 的示例代码,其中使用了 sync 包提供的 SingleFlight 对象,然后通过 Do 方法来执行请求:
func main() {
var group singleflight.Group
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
val, err, _ := group.Do("key", func() (interface{}, error) {
time.Sleep(time.Nanosecond)
return i, nil
})
fmt.Printf("result:%v error:%v\n", val, err)
}(i)
}
wg.Wait()
}
输出:
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
result:0 error:<nil>
这里函数执行需要消耗时间,在上一次Do完成前再次调用的Do不会重复执行,Do完成后向这一段时间调用Do的返回同样的值。相当于同一时刻只有一个Do会执行,把并发请求转化为单线程的。
sync.Once 主要是用在单次初始化场景中,而 SingleFlight 主要用在合并并发请求的场景中,尤其是缓存场景。
源码阅读
// call 就代表正在执行 fn 函数的请求或者是已经执行完的请求。
type call struct {
wg sync.WaitGroup
// 这个字段代表处理完的值,在waitgroup完成之前只会写一次
val interface{}
err error
dups int
chans []chan<- Result
}
// group代表一个singleflight对象
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
//如果已经存在相同的key
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
//等待这个key的第一个请求完成
c.wg.Wait()
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
//使用第一个key的请求结果
return c.val, c.err, true
}
// 第一个请求,创建一个call
c := new(call)
c.wg.Add(1)
// 加入到key map中
g.m[key] = c
g.mu.Unlock()
// 调用方法
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
应用场景
- 合并相同请求
- 解决缓存击穿