数据结构
type Group struct {
mu sync.Mutex // 互斥锁,保证并发安全
m map[string]*call // 存储相同的请求,key是相同的请求,value保存调用信息。
}
- map是懒加载,其实就是在使用时才会初始化
- mu是互斥锁,用来保证m的并发安全
type call struct {
wg sync.WaitGroup
// 存储返回值,在wg done之前只会写入一次
val interface{}
// 存储返回的错误信息
err error
// 标识别是否调用了Forgot方法
forgotten bool
// 统计相同请求的次数,在wg done之前写入
dups int
// 使用DoChan方法使用,用channel进行通知
chans []chan<- Result
}
// Dochan方法时使用
type Result struct {
Val interface{} // 存储返回值
Err error // 存储返回的错误信息
Shared bool // 标示结果是否是共享结果
}
Do 方法
// 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.
// Do执行和返回给定函数的值,确保某一个时间只有一个方法被执行。如果一个重复的请求进入,则重复的请求会等待前一个执行完毕并获取相同的数据,返回值shared标识返回值v是否是传递给重复的调用的
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 {
// 已存在则解锁,调用次数+1,
c.dups++
g.mu.Unlock()
// 然后等待 call.wg(WaitGroup) 执行完毕,只要一执行完,所有的 wait 都会被唤醒
c.wg.Wait()
// 我的Go知识还没学到异常,暂且不表:
// 这里区分 panic 错误和 runtime 的错误,避免出现死锁,后面可以看到为什么这么做[4]
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
// 如果我们没有找到这个 key 就 new call
c := new(call)
// 然后调用 waitgroup 这里只有第一次调用会 add 1,其他的都会调用 wait 阻塞掉
// 所以只要这次调用返回,所有阻塞的调用都会被唤醒
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
// 实际执行fn
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
doCall方法
// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
// 表示方法是否正常返回
normalReturn := false
recovered := false
// use double-defer to distinguish panic from runtime.Goexit,
// more details see https://golang.org/cl/134395
defer func() {
// the given function invoked runtime.Goexit
// 如果既没有正常执行完毕,又没有 recover 那就说明需要直接退出了
if !normalReturn && !recovered {
c.err = errGoexit
}
c.wg.Done()
g.mu.Lock()
defer g.mu.Unlock()
// 如果已经 forgot 过了,就不要重复删除这个 key 了
if !c.forgotten {
delete(g.m, key)
}
// 下面应该主要是异常处理的diamante
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() {
// 使用一个匿名函数来执行实际的fn
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)
}
}
}()
// 方法实际执行,将值存在c.val中
c.val, c.err = fn()
normalReturn = true
}()
if !normalReturn {
recovered = true
}
}
single_flight 图解

例子
- Do
package main
import (
"errors"
"log"
"sync"
"golang.org/x/sync/singleflight"
"time"
)
var errorNotExist = errors.New("not exist")
var g singleflight.Group
func main() {
var wg sync.WaitGroup
wg.Add(10)
//模拟10个并发
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
data, err := getData("key")
if err != nil {
log.Print(err)
return
}
log.Println(data)
}()
}
wg.Wait()
}
//获取数据
func getData(key string) (string, error) {
data, err := getDataFromCache(key)
if err == errorNotExist {
//模拟从db中获取数据
v, err, _ := g.Do(key, func() (interface{}, error) {
return getDataFromDB(key)
//set cache
})
if err != nil {
log.Println(err)
return "", err
}
//TOOD: set cache
data = v.(string)
} else if err != nil {
return "", err
}
return data, nil
}
//模拟从cache中获取值,cache中无该值
func getDataFromCache(key string) (string, error) {
return "", errorNotExist
}
//模拟从数据库中获取值
func getDataFromDB(key string) (string, error) {
log.Printf("get %s from database", key)
return "data", nil
}
//应用场景
//1. 缓存击穿:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
- DoChan
// Do chan的应用试例
func main2() {
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)
}
参考文档
https://blog.youkuaiyun.com/dingyu002/article/details/117958061
本文解析了Go语言中singleflight包的使用,通过sync.Mutex实现并发控制,确保同一时间只有一个请求执行。讲解了Do和DoChan方法,以及如何处理异常和共享结果。
501

被折叠的 条评论
为什么被折叠?



