1. SingleFlight 栅栏概述
SingleFlight 的作用是将并发请求合并成一个请求,以减少对下层服务的压力。当多个 goroutine 同时调用同一个函数的时候,只让一个 goroutine 去调用这个函数,等到这个 goroutine 返回结果的时候,再把结果返回给这几个同时调用的 goroutine,这样可以减少并发调用的数量。
如果你学会了 SingleFlight,
在面对秒杀等大并发请求的场景,而且这些请求都是读请求时,你就可以把这些请求合并为一个请求,这样,你就可以将后端服务的压力从 n 降到 1
。
尤其是在面对后端是数据库这样的服务的时候,采用 SingleFlight 可以极大地提高性能。
Go 标准库的代码中就有一个
SingleFlight
的实现,而扩展库中的 SingleFlight(golang.org/x/sync/singleflight) 就是在标准库的代码基础上改的,逻辑几乎一模一样。
1.1 SingleFlight 与 Sync.Once
标准库中的 sync.Once 也可以保证并发的 goroutine 只会执行一次函数 f,那么,SingleFlight 和 sync.Once 有什么区别呢?
sync.Once 不是只在并发的时候保证只有一个 goroutine 执行函数 f,而是会保证永远只执行一次
SingleFlight 是每次调用都重新执行,并且在多个请求同时调用的时候只有一个执行。
它们两个面对的场景是不同的,sync.Once 主要是用在单次初始化场景中,而 SingleFlight 主要用在合并并发请求的场景中,尤其是缓存场景。
2. 实现原理
SingleFlight 使用互斥锁 Mutex 和 Map 来实现。Mutex 提供并发时的读写保护,Map 用来保存同一个 key 的正在处理(in flight)的请求。SingleFlight 的数据结构是 Group,它提供了三个方法:
import "golang.org/x/sync/singleflight"
type 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)
这个方法执行一个函数,并返回函数执行的结果
需要提供一个 key,对于同一个 key,在同一时间只有一个在执行,同一个 key 并发的请求会等待。第一个执行的请求返回的结果,就是它的返回结果
函数 fn 是一个无参的函数,返回一个结果或者 error,而 Do 方法会返回函数执行的结果或者是 error
shared 会指示 v 是否返回给多个请求。
DoChan:
类似 Do 方法,只不过是返回一个 chan,等 fn 函数执行完,产生了结果以后,就能从这个 chan 中接收这个结果
Forget:
告诉 Group 忘记这个 key
这样一来,之后这个 key 请求会执行 f,而不是等待前一个未完成的 fn 函数的结果
2.1 辅助 call 对象
SingleFlight 定义一个辅助对象 call,这个 call 就代表正在执行 fn 函数的请求或者是已经执行完的请求。Group 代表 SingleFlight。
// 代表一个正在处理的请求,或者已经处理完的请求
type call struct {
wg sync.WaitGroup
// 这个字段代表处理完的值,在waitgroup完成之前只会写一次
// waitgroup完成之后就读取这个值
val interface{}
err error
// 指示当call在处理时是否要忘掉这个key
forgotten bool
dups int
chans []chan<- Result
// group代表一个singleflight对象
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized