SingleFlight模式的Go并發(fā)編程學(xué)習(xí)
最近接觸到微服務(wù)框架go-zero
,翻看了整個(gè)框架代碼,發(fā)現(xiàn)結(jié)構(gòu)清晰、代碼簡(jiǎn)潔,所以決定閱讀源碼學(xué)習(xí)下,本次閱讀的源碼位于core/syncx/singleflight.go
。
在go-zero
中SingleFlight
的作用是:將并發(fā)請(qǐng)求合并成一個(gè)請(qǐng)求,以減少對(duì)下層服務(wù)的壓力。
應(yīng)用場(chǎng)景
- 查詢緩存時(shí),合并請(qǐng)求,提升服務(wù)性能。 假設(shè)有一個(gè) IP 查詢的服務(wù),每次用戶請(qǐng)求先在緩存中查詢一個(gè) IP 的歸屬地,如果緩存中有結(jié)果則直接返回,不存在則進(jìn)行 IP 解析操作。
如上圖所示,n 個(gè)用戶請(qǐng)求查詢同一個(gè) IP(8.8.8.8)就會(huì)對(duì)應(yīng) n 個(gè) Redis 的查詢,在高并發(fā)場(chǎng)景下,如果能將 n 個(gè) Redis 查詢合并成一個(gè) Redis 查詢,那么性能肯定會(huì)提升很多,而 SingleFlight
就是用來(lái)實(shí)現(xiàn)請(qǐng)求合并的,效果如下:
- 防止緩存擊穿。
緩存擊穿問(wèn)題是指:在高并發(fā)的場(chǎng)景中,大量的請(qǐng)求同時(shí)查詢一個(gè) key ,如果這個(gè) key 正好過(guò)期失效了,就會(huì)導(dǎo)致大量的請(qǐng)求都打到數(shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)的連接增多,負(fù)載上升。
通過(guò)SingleFlight
可以將對(duì)同一個(gè)Key的并發(fā)請(qǐng)求進(jìn)行合并,只讓其中一個(gè)請(qǐng)求到數(shù)據(jù)庫(kù)進(jìn)行查詢,其他請(qǐng)求共享同一個(gè)結(jié)果,可以很大程度提升并發(fā)能力。
應(yīng)用方式
直接上代碼:
func main() { round := 10 var wg sync.WaitGroup barrier := syncx.NewSingleFlight() wg.Add(round) for i := 0; i < round; i++ { go func() { defer wg.Done() // 啟用10個(gè)協(xié)程模擬獲取緩存操作 val, err := barrier.Do("get_rand_int", func() (interface{}, error) { time.Sleep(time.Second) return rand.Int(), nil }) if err != nil { fmt.Println(err) } else { fmt.Println(val) } }() } wg.Wait() }
以上代碼,模擬 10 個(gè)協(xié)程請(qǐng)求 Redis 獲取一個(gè) key 的內(nèi)容,代碼很簡(jiǎn)單,就是執(zhí)行Do()
方法。其中,接收兩個(gè)參數(shù),第一個(gè)參數(shù)是獲取資源的標(biāo)識(shí),可以是 redis 中緩存的 key,第二個(gè)參數(shù)就是一個(gè)匿名函數(shù),封裝好要做的業(yè)務(wù)邏輯。最終獲得的結(jié)果如下:
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
5577006791947779410
從上看出,10個(gè)協(xié)程都獲得了同一個(gè)結(jié)果,也就是只有一個(gè)協(xié)程真正執(zhí)行了rand.Int()
獲取了隨機(jī)數(shù),其他的協(xié)程都共享了這個(gè)結(jié)果。
源碼解析
先看代碼結(jié)構(gòu):
type ( // 定義接口,有2個(gè)方法 Do 和 DoEx,其實(shí)邏輯是一樣的,DoEx 多了一個(gè)標(biāo)識(shí),主要看Do的邏輯就夠了 SingleFlight interface { Do(key string, fn func() (interface{}, error)) (interface{}, error) DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error) } // 定義 call 的結(jié)構(gòu) call struct { wg sync.WaitGroup // 用于實(shí)現(xiàn)通過(guò)1個(gè) call,其他 call 阻塞 val interface{} // 表示 call 操作的返回結(jié)果 err error // 表示 call 操作發(fā)生的錯(cuò)誤 } // 總控結(jié)構(gòu),實(shí)現(xiàn) SingleFlight 接口 flightGroup struct { calls map[string]*call // 不同的 call 對(duì)應(yīng)不同的 key lock sync.Mutex // 利用鎖控制請(qǐng)求 } )
然后看最核心的Do方法
做了什么事情:
func (g *flightGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) { c, done := g.createCall(key) if done { return c.val, c.err } g.makeCall(c, key, fn) return c.val, c.err }
代碼很簡(jiǎn)潔,利用g.createCall(key)
對(duì) key 發(fā)起 call 請(qǐng)求(其實(shí)就是做一件事情),如果此時(shí)已經(jīng)有其他協(xié)程已經(jīng)在發(fā)起 call 請(qǐng)求就阻塞?。╠one 為 true 的情況),等待拿到結(jié)果后直接返回。如果 done 是 false,說(shuō)明當(dāng)前協(xié)程是第一個(gè)發(fā)起 call 的協(xié)程,那么就執(zhí)行g.makeCall(c, key, fn)
真正地發(fā)起 call 請(qǐng)求(此后的其他協(xié)程就阻塞在了g.createCall(key)
)。
從上圖可知,其實(shí)關(guān)鍵就兩步:
- 判斷是第一個(gè)請(qǐng)求的協(xié)程(利用map)
- 阻塞住其他所有協(xié)程(利用 sync.WaitGroup)
來(lái)看下g.createCall(key)
如何實(shí)現(xiàn)的:
func (g *flightGroup) createCall(key string) (c *call, done bool) { g.lock.Lock() if c, ok := g.calls[key]; ok { g.lock.Unlock() c.wg.Wait() return c, true } c = new(call) c.wg.Add(1) g.calls[key] = c g.lock.Unlock() return c, false }
先看第一步:判斷是第一個(gè)請(qǐng)求的協(xié)程(利用map)
g.lock.Lock() if c, ok := g.calls[key]; ok { g.lock.Unlock() c.wg.Wait() return c, true }
此處判斷 map 中的 key 是否存在,如果已經(jīng)存在,說(shuō)明已經(jīng)有其他協(xié)程在請(qǐng)求了,當(dāng)前這個(gè)協(xié)程只需要等待,等待是利用了sync.WaitGroup
的Wait()
方法實(shí)現(xiàn)的,此處還是很巧妙的。要注意的是,map 在 Go 中是非并發(fā)安全的,所以需要加鎖。
再看第二步:阻塞住其他所有協(xié)程(利用 sync.WaitGroup)
c = new(call) c.wg.Add(1) g.calls[key] = c
因?yàn)槭堑谝粋€(gè)發(fā)起 call 的協(xié)程,所以需要 new 這個(gè) call,然后將wg.Add(1)
,這樣就對(duì)應(yīng)了上面的wg.Wait()
,阻塞剩下的協(xié)程。隨后將 new 的 call 放入 map 中,注意此時(shí)只是完成了初始化,并沒(méi)有真正去執(zhí)行call請(qǐng)求,真正的處理邏輯在 g.makeCall(c, key, fn)
中。
func (g *flightGroup) makeCall(c *call, key string, fn func() (interface{}, error)) { defer func() { g.lock.Lock() delete(g.calls, key) g.lock.Unlock() c.wg.Done() }() c.val, c.err = fn() }
這個(gè)方法中做的事情很簡(jiǎn)單,就是執(zhí)行了傳遞的匿名函數(shù)fn()
(也就是真正call請(qǐng)求要做的事情)。最后處理收尾的事情(通過(guò)defer),也是分成兩步:
- 刪除 map 中的 key,使得下次發(fā)起請(qǐng)求可以獲取新的值。
- 調(diào)用
wg.Done()
,讓之前阻塞的協(xié)程全部獲得結(jié)果并返回。
至此,SingleFlight
的核心代碼就解析完畢了,雖然代碼不長(zhǎng),但是這個(gè)思想還是很棒的,可以在實(shí)際工作中借鑒。
總結(jié)
- map 非并發(fā)安全,記得加鎖。
- 巧用 sync.WaitGroup 去完成需要阻塞控制協(xié)程的應(yīng)用場(chǎng)景。
- 通過(guò)匿名函數(shù) fn 去封裝傳遞具體業(yè)務(wù)邏輯,在調(diào)用 fn 的上層函數(shù)中去完成統(tǒng)一的邏輯處理。
項(xiàng)目地址
https://github.com/zeromicro/go-zero
以上就是SingleFlight模式的Go并發(fā)編程學(xué)習(xí)的詳細(xì)內(nèi)容,更多關(guān)于SingleFlight模式Go并發(fā)編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang validator參數(shù)校驗(yàn)的實(shí)現(xiàn)
這篇文章主要介紹了golang validator參數(shù)校驗(yàn)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10GO 使用Webhook 實(shí)現(xiàn)github 自動(dòng)化部署的方法
這篇文章主要介紹了GO 使用Webhook 實(shí)現(xiàn)github 自動(dòng)化部署的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)單網(wǎng)絡(luò)端口掃描方法
這篇文章主要介紹了Go語(yǔ)言實(shí)現(xiàn)的簡(jiǎn)單網(wǎng)絡(luò)端口掃描方法,實(shí)例分析了Go語(yǔ)言網(wǎng)絡(luò)程序的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Go語(yǔ)言實(shí)現(xiàn)自動(dòng)填寫古詩(shī)詞實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言實(shí)現(xiàn)自動(dòng)填寫古詩(shī)詞的相關(guān)資料,這是最近在項(xiàng)目中遇到的一個(gè)需求,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03golang中sync.Map并發(fā)創(chuàng)建、讀取問(wèn)題實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于golang中sync.Map并發(fā)創(chuàng)建、讀取問(wèn)題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07利用Go語(yǔ)言實(shí)現(xiàn)在終端繪制小兔子
這篇文章主要為大家詳細(xì)介紹了如何利用Go語(yǔ)言實(shí)現(xiàn)在終端繪制小兔子來(lái)給大家拜個(gè)早年,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-01-01golang 數(shù)組隨機(jī)排序的實(shí)現(xiàn)
本文主要介紹了golang 數(shù)組隨機(jī)排序的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12