SingleFlight
总结
- 提供了三个参数
Do同步通知
、Dochan异步chan通知
、Forget删除map的存在的键值
- 每次调用都会重置map
- 结构体是由
mu sync.Mutex
、m map[string]*call
组成,mu用于操作的排它锁,m用于存储已有的键值的个数 - 如果m中存在当前键值对,就会阻塞等待,不存在的时候会加创建call结构存入m中
- Dochan会异步执行,将结果放入chan通知其他阻塞的协程
- 阻塞的协程永的WaitGroup实现
do 的简单使用
package main
import (
"time"
"golang.org/x/sync/singleflight"
"log"
)
func main() {
var singleSetCache singleflight.Group
getAndSetCache := func(requestID int, cacheKey string) (string, error) {
log.Printf("request %v start to get and set cache...", requestID)
value, _, _ := singleSetCache.Do(cacheKey, func() (ret interface{}, err error) { //do的入参key,可以直接使用缓存的key,这样同一个缓存,只有一个协程会去读DB
log.Printf("request %v is setting cache...", requestID)
time.Sleep(3 * time.Second)
log.Printf("request %v set cache success!", requestID)
return "VALUE", nil
})
return value.(string), nil
}
cacheKey := "cacheKey"
for i := 1; i < 10; i++ { //模拟多个协程同时请求
go func(requestID int) {
value, _ := getAndSetCache(requestID, cacheKey)
log.Printf("request %v get value: %v", requestID, value)
}(i)
}
time.Sleep(20 * time.Second)
for i := 1; i < 10; i++ { //模拟多个协程同时请求
go func(requestID int) {
value, _ := getAndSetCache(requestID, cacheKey)
log.Printf("request %v get value: %v", requestID, value)
}(i)
}
time.Sleep(20 * time.Second)
}
2020/09/28 14:12:11 request 3 start to get and set cache...
2020/09/28 14:12:11 request 3 is setting cache...
2020/09/28 14:12:11 request 1 start to get and set cache...
2020/09/28 14:12:11 request 2 start to get and set cache...
2020/09/28 14:12:11 request 9 start to get and set cache...
2020/09/28 14:12:11 request 6 start to get and set cache...
2020/09/28 14:12:11 request 7 start to get and set cache...
2020/09/28 14:12:11 request 4 start to get and set cache...
2020/09/28 14:12:11 request 5 start to get and set cache...
2020/09/28 14:12:11 request 8 start to get and set cache...
2020/09/28 14:12:14 request 3 set cache success!
2020/09/28 14:12:14 request 3 get value: VALUE
2020/09/28 14:12:14 request 8 get value: VALUE
2020/09/28 14:12:14 request 1 get value: VALUE
2020/09/28 14:12:14 request 2 get value: VALUE
2020/09/28 14:12:14 request 9 get value: VALUE
2020/09/28 14:12:14 request 6 get value: VALUE
2020/09/28 14:12:14 request 7 get value: VALUE
2020/09/28 14:12:14 request 4 get value: VALUE
2020/09/28 14:12:14 request 5 get value: VALUE
2020/09/28 14:12:31 request 9 start to get and set cache...
2020/09/28 14:12:31 request 9 is setting cache...
2020/09/28 14:12:31 request 5 start to get and set cache...
2020/09/28 14:12:31 request 6 start to get and set cache...
2020/09/28 14:12:31 request 7 start to get and set cache...
2020/09/28 14:12:31 request 8 start to get and set cache...
2020/09/28 14:12:31 request 4 start to get and set cache...
2020/09/28 14:12:31 request 2 start to get and set cache...
2020/09/28 14:12:31 request 1 start to get and set cache...
2020/09/28 14:12:31 request 3 start to get and set cache...
2020/09/28 14:12:34 request 9 set cache success!
2020/09/28 14:12:34 request 9 get value: VALUE
2020/09/28 14:12:34 request 3 get value: VALUE
2020/09/28 14:12:34 request 4 get value: VALUE
2020/09/28 14:12:34 request 2 get value: VALUE
2020/09/28 14:12:34 request 8 get value: VALUE
2020/09/28 14:12:34 request 5 get value: VALUE
2020/09/28 14:12:34 request 7 get value: VALUE
2020/09/28 14:12:34 request 6 get value: VALUE
2020/09/28 14:12:34 request 1 get value: VALUE
Process finished with exit code 0
每一次调用do都会重新开始,内存不会持久化支持的map集合
docall 的简单使用
package main
import (
"errors"
"golang.org/x/sync/singleflight"
"log"
"time"
)
func main() {
var singleSetCache singleflight.Group
getAndSetCache := func(requestID int, cacheKey string) (string, error) {
log.Printf("request %v start to get and set cache...", requestID)
retChan := singleSetCache.DoChan(cacheKey, func() (ret interface{}, err error) {
log.Printf("request %v is setting cache...", requestID)
time.Sleep(3 * time.Second)
log.Printf("request %v set cache success!", requestID)
return "VALUE", nil
})
var ret singleflight.Result
timeout := time.After(5 * time.Second)
select { //加入了超时机制
case <-timeout:
log.Printf("time out!")
return "", errors.New("time out")
case ret = <-retChan: //从chan中取出结果
return ret.Val.(string), ret.Err
}
return "", nil
}
cacheKey := "cacheKey"
for i := 1; i < 10; i++ {
go func(requestID int) {
value, _ := getAndSetCache(requestID, cacheKey)
log.Printf("request %v get value: %v", requestID, value)
}(i)
}
time.Sleep(20 * time.Second)
}
2020/09/28 14:19:13 request 4 start to get and set cache...
2020/09/28 14:19:13 request 2 start to get and set cache...
2020/09/28 14:19:13 request 3 start to get and set cache...
2020/09/28 14:19:13 request 8 start to get and set cache...
2020/09/28 14:19:13 request 1 start to get and set cache...
2020/09/28 14:19:13 request 9 start to get and set cache...
2020/09/28 14:19:13 request 5 start to get and set cache...
2020/09/28 14:19:13 request 7 start to get and set cache...
2020/09/28 14:19:13 request 6 start to get and set cache...
2020/09/28 14:19:13 request 4 is setting cache...
2020/09/28 14:19:16 request 4 set cache success!
2020/09/28 14:19:16 request 6 get value: VALUE
2020/09/28 14:19:16 request 4 get value: VALUE
2020/09/28 14:19:16 request 2 get value: VALUE
2020/09/28 14:19:16 request 3 get value: VALUE
2020/09/28 14:19:16 request 8 get value: VALUE
2020/09/28 14:19:16 request 1 get value: VALUE
2020/09/28 14:19:16 request 9 get value: VALUE
2020/09/28 14:19:16 request 5 get value: VALUE
2020/09/28 14:19:16 request 7 get value: VALUE
Process finished with exit code 0
每一次调用dochan都会重新开始,内存不会持久化支持的map集合,只不过将结果挡在chan中进行传输
dochan源码
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
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)//可以看到,每个等待的协程,都有一个结果channel。从之前的g.doCall里也可以看到,每个channel都给塞了结果。为什么不所有协程共用一个channel?因为那样就得在channel里塞至少与协程数量一样的结果数量,但是你却无法保证用户一个协程只读取一次。
g.mu.Unlock()
return ch
}
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
}
singleflight 是 Go 语言扩展包中提供了另一种同步原语,这其实也是作者最喜欢的一种同步扩展机制,它能够在一个服务中抑制对下游的多次重复请求,一个比较常见的使用场景是 — 我们在使用 Redis 对数据库中的一些热门数据进行了缓存并设置了超时时间,缓存超时的一瞬间可能有非常多的并行请求发现了 Redis 中已经不包含任何缓存所以大量的流量会打到数据库上影响服务的延时和质量。
但是 singleflight
就能有效地解决这个问题,它的主要作用就是对于同一个 Key
最终只会进行一次函数调用,在这个上下文中就是只会进行一次数据库查询,查询的结果会写回 Redis 并同步给所有请求对应 Key
的用户:
这其实就减少了对下游的瞬时流量,在获取下游资源非常耗时,例如:访问缓存、数据库等场景下就非常适合使用 singleflight
对服务进行优化,在上述的这个例子中我们就可以在想 Redis 和数据库中获取数据时都使用 singleflight
提供的这一功能减少下游的压力;它的使用其实也非常简单,我们可以直接使用 singleflight.Group{}
创建一个新的 Group
结构体,然后通过调用 Do
方法就能对相同的请求进行抑制:
结构体
Group
结构体本身由一个互斥锁 Mutex
和一个从 Key
到 call
结构体指针的映射表组成,每一个 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<- Result
}
-
call
结构体中的val
和err
字段都是在执行传入的函数时只会被赋值一次,它们也只会在WaitGroup
等待结束都被读取, -
而
dups
和chans
字段分别用于存储当前singleflight
抑制的请求数量以及在结果返回时将信息传递给调用方。
调用流程
每次 Do
方法的调用时都会获取互斥锁并尝试对 Group
持有的映射表进行懒加载,随后判断是否已经存在 key
对应的函数调用:
- 当不存在对应的
call
结构体时:- 初始化一个新的
call
结构体指针; - 增加
WaitGroup
持有的计数器; - 将
call
结构体指针添加到映射表; - 释放持有的互斥锁
Mutex
; - 阻塞地调用
doCall
方法等待结果的返回;
- 初始化一个新的
- 当已经存在对应的
call
结构体时;- 增加
dups
计数器,它表示当前重复的调用次数; - 释放持有的互斥锁
Mutex
; - 通过
WaitGroup.Wait
等待请求的返回;
- 增加
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)
}
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
}
因为 val
和 err
两个字段都只会在 doCall
方法中被赋值,所以当 doCall
方法和 WaitGroup.Wait
方法返回时,这两个值就会返回给 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 <- Result{c.val, c.err, c.dups > 0}
}
g.mu.Unlock()
}
doCall
中会运行传入的函数 fn
,该函数的返回值就会赋值给 c.val
和 c.err
,函数执行结束后就会调用 WaitGroup.Done
方法通知所有被抑制的请求,当前函数已经执行完成,可以从 call
结构体中取出返回值并返回了;在这之后,doCall
方法会获取持有的互斥锁并通过管道将信息同步给使用 DoChan
方法的调用方。
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
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
}
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
}
DoChan
方法和 Do
的区别就是,它使用 Goroutine 异步执行 doCall
并向 call
持有的 chans
切片中追加 chan Result
变量,这也是它能够提供异步传值的原因。