问题提出
一般业务场景下,为了保证系统的稳定性,一般在mysql前加一层redis缓存(设置一定的超时时间)。所以请求某个key的时候,如果命中redis缓存则直接读取缓存,如果不命中就访问mysql。当某个热 key 缓存失效时, 会有大量的请求直接请求mysql导致缓存击穿。
singleflight 便可以解决上述问题本文研究的singleflight包路径位于 src/internal/singleflight/singleflight.go
Go 还有一种实现 golang.org/x/sync/sing...
核心结构
Package singleflight provides a duplicate function call suppression mechanism.
通过强制一个函数所有后续调用等待第一个调用完成,消除同时运行重复函数的低效性
-
call
封装singleflight中的一次调用,其中val
fn 返回的数据err
fn 返回的errshared
表明返回数据是调用 fn 得到的还是其他请求调用返回的chans
配合DoChan使用
-
Group
封装基于key维度的 重复调用消除
go
// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val any
err error
// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
dups int
chans []chan<- Result
}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
Val any
Err error
Shared bool
}
Do
Do 执行并返回给定函数
fn
的结果,同一个key的调用,保证只有一个执行在进行中。如果存在重复调用,重复的调用者会等待原始执行完成并接收相同的结果
- 基于该key 查询是否存在
in-flight
调用,如果存在则阻塞其等待完成 - 反之,将该key存入
m
,同步调用doCall
执行传入的方法,方法执行完成删除对应的key - 如果
Group
中chan不为空,则将结果封装成Result
发送到对应的通道(DoChan中 会使用)
go
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group) Do(key string, fn func() (any, error)) (v any, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err, true
}
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
}
// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (any, error)) {
c.val, c.err = fn()
c.wg.Done()
g.mu.Lock()
delete(g.m, key)
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0 }
}
g.mu.Unlock()
}
DoChan
Do 请求是阻塞等待的,存在以下不足
- 请求是阻塞的,没有超时控制,难以快速失败
- 虽然抑制了重复调用的并发,但是成功率可能会因此下降
DoChan 可以解决这一问题,功能类似于Do方法
区别在于 该方法不会阻塞地等待结果的返回,而是直接返回一个通道
- 基于该key 查询是否存在
in-flight
调用,如果存在则初始化长度为1的带缓冲通道ch
,将该通道保存到call.chans
中,当fn
执行完成后会将结果发送到这些通道中 - 异步执行
doCall
- 返回初始化的通道
ch
(等数据就绪了 数据会返回到对应通道中)
go
// DoChan is like Do but returns a channel that will receive the
// results when they are ready. The second result is true if the function
// will eventually be called, false if it will not (because there is
// a pending request with this key).
func (g *Group) DoChan(key string, fn func() (any, error)) (<-chan Result, bool) {
// 缓冲区长度为1 的通道 ==> 数据的发送不会阻塞
ch := make(chan Result, 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
c.chans = append(c.chans, ch)
g.mu.Unlock()
return ch, false
}
c := &call{chans: []chan<- Result{ch}}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
go g.doCall(c, key, fn)
return ch, true
}
使用场景伪代码
go
func doLongTimeExec(ctx context.Context) error {
g * singleflight.Group
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ch := g.DoChan("doLongTimeExec", func() (any, error) {
// 模拟耗时操作
time.Sleep(time.Second * 2)
return nil, nil
})
select {
case <-ctx.Done():
return ctx.Err()
case ret := <-ch:
return ret.Err
}
}
ForgetUnshared
想象一种场景,使用
Do
/DoChan
虽然说降低了对下游的重复请求,但是单次请求失败/超时 产生的影响面被扩大了(因为并发场景下其他请求会复用这个结果) 这时候便可以通过异步调用 ForgetUnshared 改善这一情况ForgetUnshared: 失效 key,后续对此 key 的调用将执行 fn,而不是等待前面的调用完成
- 如果不存在该key的调用直接返回
- 如果不存在其他请求在等该结果的返回,则删除对应的key(这样后续的key则会执行对应的方法
fn
,而不是等待前面的调用完成) - 如果存在其他请求在等该结果的返回,这时候不直接删除对应的
call
go
// ForgetUnshared tells the singleflight to forget about a key if it is not
// shared with any other goroutines. Future calls to Do for a forgotten key
// will call the function rather than waiting for an earlier call to complete.
// Returns whether the key was forgotten or unknown--that is, whether no
// other goroutines are waiting for the result.
func (g *Group) ForgetUnshared(key string) bool {
g.mu.Lock()
defer g.mu.Unlock()
c, ok := g.m[key]
if !ok {
return true
}
if c.dups == 0 {
delete(g.m, key)
return true
}
return false
}