Go底層之singleflight包原理分析
背景
在處理同一時(shí)刻接口的并發(fā)請(qǐng)求時(shí),常見的有這幾種情況:
一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求等待順序執(zhí)行,使用互斥鎖就能完成、一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求都丟棄、一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求等待拿取相同的結(jié)果。
使用singleflight包就能達(dá)到一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求過來等待第一個(gè)請(qǐng)求執(zhí)行完,然后共享第一個(gè)請(qǐng)求的結(jié)果,在處理并發(fā)場(chǎng)景時(shí)非常好用。
下載
go get -u golang.org/x/sync/singleflight
原理解釋
- singleflight底層結(jié)構(gòu):
type Group struct { mu sync.Mutex //保護(hù)map對(duì)象m的并發(fā)安全 m map[string]*call //key-請(qǐng)求的唯一標(biāo)識(shí),val-請(qǐng)求唯一標(biāo)識(shí)對(duì)應(yīng)要執(zhí)行的函數(shù) }
- call底層結(jié)構(gòu):
type call struct { wg sync.WaitGroup //用來阻塞相同標(biāo)識(shí)對(duì)應(yīng)的請(qǐng)求中的第一個(gè)請(qǐng)求之外的請(qǐng)求 val interface{} //第一個(gè)請(qǐng)求的執(zhí)行結(jié)果 err error //第一個(gè)請(qǐng)求返回的錯(cuò)誤 dups int //第一個(gè)請(qǐng)求之外的其它請(qǐng)求數(shù) chans []chan<- Result //請(qǐng)求結(jié)果寫入通道 }
- 關(guān)鍵函數(shù):
// // Do // @Description: 一個(gè)唯一標(biāo)識(shí)對(duì)應(yīng)的請(qǐng)求在執(zhí)行過程中,相同唯一標(biāo)識(shí)對(duì)應(yīng)的請(qǐng)求會(huì)被阻塞,等待第一個(gè)請(qǐng)求執(zhí)行完并共享結(jié)果 // @receiver g // @param key 請(qǐng)求唯一標(biāo)識(shí) // @param fn 請(qǐng)求要執(zhí)行的函數(shù) // @return v 請(qǐng)求要執(zhí)行函數(shù)返回的結(jié)果 // @return err 請(qǐng)求要執(zhí)行的函數(shù)返回的錯(cuò)誤 // @return shared 是否有多個(gè)請(qǐng)求共享結(jié)果 // func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { g.mu.Lock() //保護(hù)map對(duì)象m的并發(fā)安全 if g.m == nil { g.m = make(map[string]*call) //初始化m對(duì)象 } if c, ok := g.m[key]; ok { //map中key存在說明這個(gè)key對(duì)應(yīng)的請(qǐng)求正在執(zhí)行中,這次請(qǐng)求不是第一個(gè)請(qǐng)求 c.dups++ //等待共享結(jié)果數(shù)+1 g.mu.Unlock() c.wg.Wait() //阻塞等待第一個(gè)請(qǐng)求執(zhí)行完 if e, ok := c.err.(*panicError); ok { panic(e) } else if c.err == errGoexit { runtime.Goexit() } return c.val, c.err, true //返回第一個(gè)請(qǐng)求的執(zhí)行結(jié)果 } //第一個(gè)請(qǐng)求的處理邏輯 c := new(call) c.wg.Add(1) //計(jì)數(shù)+1 g.m[key] = c //唯一標(biāo)識(shí)關(guān)聯(lián)對(duì)應(yīng)的函數(shù)對(duì)象 g.mu.Unlock() g.doCall(c, key, fn) //執(zhí)行請(qǐng)求對(duì)應(yīng)的函數(shù) return c.val, c.err, c.dups > 0 //返回執(zhí)行結(jié)果 }
上面Do函數(shù)中g(shù).doCall函數(shù)也需要大概理解一下,就是會(huì)將key對(duì)應(yīng)函數(shù)的執(zhí)行結(jié)果寫的call對(duì)象c里,然后清空wg計(jì)數(shù),相同key對(duì)應(yīng)的其它請(qǐng)求就會(huì)跳出c.wg.Wait()阻塞,直接從call對(duì)象c中讀取第一個(gè)請(qǐng)求的執(zhí)行結(jié)果和錯(cuò)誤信息并返回,源碼如下:
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { normalReturn := false recovered := false defer func() { //fn函數(shù)執(zhí)行完之后執(zhí)行 if !normalReturn && !recovered { c.err = errGoexit } g.mu.Lock() defer g.mu.Unlock() c.wg.Done() //釋放計(jì)數(shù) if g.m[key] == c { delete(g.m, key) //刪除此次請(qǐng)求的唯一標(biāo)識(shí)相關(guān)信息 } if e, ok := c.err.(*panicError); ok { if len(c.chans) > 0 { go panic(e) select {} } else { panic(e) } } else if c.err == errGoexit { // Already in the process of goexit, no need to call again } else { for _, ch := range c.chans { //執(zhí)行結(jié)果寫入通道 ch <- Result{c.val, c.err, c.dups > 0} } } }() func() { defer func() { if !normalReturn { if r := recover(); r != nil { c.err = newPanicError(r) } } }() c.val, c.err = fn() //執(zhí)行fn函數(shù) normalReturn = true }() if !normalReturn { recovered = true } }
singleflight中還提供了與Do函數(shù)功能相同的函數(shù)DoChan函數(shù),唯一區(qū)別就是將請(qǐng)求對(duì)應(yīng)的函數(shù)執(zhí)行結(jié)果放到通道中進(jìn)行返回,這兩函數(shù)一個(gè)用于同步場(chǎng)景,一個(gè)用于異步場(chǎng)景。還有一個(gè)Forget函數(shù):
func (g *Group) Forget(key string) { g.mu.Lock() delete(g.m, key) //刪除map中的key,相同key對(duì)應(yīng)請(qǐng)求進(jìn)來會(huì)重新執(zhí)行,不等待第一個(gè)key對(duì)應(yīng)請(qǐng)求的執(zhí)行結(jié)果 g.mu.Unlock() }
代碼示例
- 示例如下:
func main() { var singleFlight singleflight.Group //初始化一個(gè)單次執(zhí)行對(duì)象 var count uint64 //用于測(cè)試是否被修改 //為了并發(fā)執(zhí)行,這里測(cè)試唯一標(biāo)識(shí)都為xxx go func() { val1, _, shared1 := singleFlight.Do("xxx", func() (interface{}, error) { logger.Info("first count +1") atomic.AddUint64(&count, 1) //第一次執(zhí)行,將count+1 time.Sleep(5 * time.Second) //增加第一次執(zhí)行時(shí)間 return count, nil }) //打印第一次執(zhí)行結(jié)果 logger.Info("first count info", zap.Any("val1", val1), zap.Bool("shared1", shared1), zap.Uint64("count", count)) }() time.Sleep(2 * time.Second) //為了防止下面的Do函數(shù)先執(zhí)行 val2, _, shared2 := singleFlight.Do("xxx", func() (interface{}, error) { logger.Info("second count +1") atomic.AddUint64(&count, 1) //第2次執(zhí)行count+1 return count, nil }) //打印第二次執(zhí)行結(jié)果 logger.Info("second count info", zap.Any("val2", val2), zap.Bool("shared2", shared2), zap.Uint64("count", count)) }
- 控制臺(tái)輸出:
$ go run ./singlefight_demo/main.go [2025-01-09 17:08:11.169] | INFO | Goroutine:6 | [singlefight_demo/main.go:19] | first count +1 [2025-01-09 17:08:16.261] | INFO | Goroutine:6 | [singlefight_demo/main.go:28] | first count info | {"val1": 1, "shared1": true, "count": 1} [2025-01-09 17:08:16.261] | INFO | Goroutine:1 | [singlefight_demo/main.go:41] | second count info | {"val2": 1, "shared2": true, "count": 1}
總結(jié)
看singleflight原碼之后,要實(shí)現(xiàn)一個(gè)請(qǐng)求正在執(zhí)行,相同的其它請(qǐng)求進(jìn)來時(shí)直接報(bào)錯(cuò)的功能也很簡(jiǎn)單,將singleflight中等待第一個(gè)請(qǐng)求的邏輯改為直接返回錯(cuò)誤就可以。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
如何使用?Go?和?Excelize?構(gòu)建電子表格
這篇文章主要介紹了如何使用Go和Excelize構(gòu)建電子表格,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09Go Mongox輕松實(shí)現(xiàn)MongoDB的時(shí)間字段自動(dòng)填充
這篇文章主要為大家詳細(xì)介紹了Go語言如何使用 mongox 庫(kù),在插入和更新數(shù)據(jù)時(shí)自動(dòng)填充時(shí)間字段,從而提升開發(fā)效率并減少重復(fù)代碼,需要的可以參考下2025-02-02實(shí)現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫(kù)操作示例詳解
這篇文章主要為大家介紹了實(shí)現(xiàn)像php一樣方便的go ORM數(shù)據(jù)庫(kù)操作示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法
這篇文章主要介紹了GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01淺析Go語言如何在select語句中實(shí)現(xiàn)優(yōu)先級(jí)
這篇文章主要為大家詳細(xì)介紹了Go語言如何在select語句中實(shí)現(xiàn)優(yōu)先級(jí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03golang的強(qiáng)制類型轉(zhuǎn)換實(shí)現(xiàn)
這篇文章主要介紹了golang的強(qiáng)制類型轉(zhuǎn)換實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02