Go擴(kuò)展原語之SingleFlight的用法詳解
概述
singleflight.Group 是 Go 語言擴(kuò)展包的另一種同步原語,它能夠再一個(gè)服務(wù)中抑制對(duì)下游的多次重復(fù)請求。一個(gè)比較常見的使用場景是,我們使用 Redis 對(duì)數(shù)據(jù)庫中的數(shù)據(jù)進(jìn)行緩存,發(fā)生緩存擊穿時(shí),大量請求會(huì)打到數(shù)據(jù)庫上進(jìn)而影響服務(wù)的尾延時(shí)。
而 singleflight.Group 能夠有效地解決這個(gè)問題,它能夠限制對(duì)同一個(gè)鍵值對(duì)的多次重復(fù)請求,減少對(duì)下游的瞬時(shí)流量。

在資源的獲取非常昂貴時(shí)(例如訪問緩存、數(shù)據(jù)庫),就很適合使用 singleflight.Group 優(yōu)化服務(wù)。它的使用方法如下:
type service struct {
requestGroup singleflight.Group
}
func (s *service) handleRequest(ctx context.Context, request Request) (Response, error) {
v, err, _ := requestGroup.Do(request.Hash(), func() (interface{}, error) {
rows, err := // select * from tables
if err != nil {
return nil, err
}
return rows, nil
})
if err != nil {
return nil, err
}
return Response{
rows: rows,
}, nil
}因?yàn)檎埱蟮墓T跇I(yè)務(wù)上一般表示相同的請求,所以上述代碼使用它作為請求的鍵。當(dāng)然,我們也可以選擇其他的字段作為 singleflight.Group.Do 方法的第一個(gè)參數(shù)減少重復(fù)的請求。
結(jié)構(gòu)體
singleflight.Group 結(jié)構(gòu)體由一個(gè)互斥鎖 sync.Mutex 和一個(gè)映射表組成,每一個(gè) singleflight.call 結(jié)構(gòu)體都保存了當(dāng)前調(diào)用對(duì)應(yīng)的信息:
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
}singleflight.call 結(jié)構(gòu)體中的 val 和 err 字段都只會(huì)在執(zhí)行傳入的函數(shù)時(shí)賦值一次并在 sync.WaitGroup.Wait 返回時(shí)被讀取。
dups 和 chans 兩個(gè)字段分別存儲(chǔ)了抑制的請求數(shù)量以及用于同步結(jié)果的 Channel。
接口
singleflight.Group求的方法:
singleflight.Group.Do— 同步等待的方法;singleflight.Group.DoChan— 返回 Channel 異步等待的方法;
這兩個(gè)方法在功能上沒有太多的區(qū)別,只是在接口的表現(xiàn)上稍有不同。
每次調(diào)用 singleflight.Group.Do 方法時(shí)都會(huì)獲取互斥鎖,隨后判斷是否已經(jīng)存在鍵對(duì)應(yīng)的 singleflight.call:
當(dāng)不存在對(duì)應(yīng)的
singleflight.call時(shí):- 初始化一個(gè)新的
singleflight.call指針 - 增加
sync.WaitGroup持有的計(jì)數(shù)器 - 將
singleflight.call指針添加到映射表 - 釋放持有的互斥鎖
- 阻塞地調(diào)用
singleflight.Group.doCall方法等待結(jié)果的返回
- 初始化一個(gè)新的
當(dāng)存在對(duì)應(yīng)的
singleflight.call時(shí):- 增加
dups計(jì)數(shù)器,它表示當(dāng)前重復(fù)的調(diào)用次數(shù) - 釋放持有的互斥鎖
- 通過
sync.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
}因?yàn)?nbsp;val 和 err 兩個(gè)字段都只會(huì)在 singleflight.Group.doCall 方法中賦值,所以當(dāng) singleflight.Group.doCall 和 sync.WaitGroup.Wait 返回時(shí),函數(shù)調(diào)用的結(jié)果和錯(cuò)誤都會(huì)返回給 singleflight.Group.Do 的調(diào)用方。
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()
}- 行傳入的函數(shù)
fn,該函數(shù)的返回值會(huì)賦值給c.val和c.err - 調(diào)用
sync.WaitGroup.Done方法通知所有等待結(jié)果的Goroutine— 當(dāng)前函數(shù)已經(jīng)執(zhí)行完成,可以從call結(jié)構(gòu)體中取出返回值并返回了 - 獲取持有的互斥鎖并通過管道將信息同步給使用
singleflight.Group.DoChan方法的Goroutine
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
}singleflight.Group.Do 和 singleflight.Group.DoChan 分別提供了同步和異步的調(diào)用方式,這讓我們使用起來也更加靈活。
小結(jié)
當(dāng)我們需要減少對(duì)下游的相同請求時(shí),可以使用 singleflight.Group 來增加吞吐量和服務(wù)質(zhì)量,不過在使用的過程中我們也需要注意以下的幾個(gè)問題:
singleflight.Group.Do和singleflight.Group.DoChan一個(gè)用于同步阻塞調(diào)用傳入的函數(shù),一個(gè)用于異步調(diào)用傳入的參數(shù)并通過 Channel 接收函數(shù)的返回值singleflight.Group.Forget可以通知singleflight.Group在持有的映射表中刪除某個(gè)鍵,接下來對(duì)該鍵的調(diào)用就不會(huì)等待前面的函數(shù)返回了- 一旦調(diào)用的函數(shù)返回了錯(cuò)誤,所有在等待的
Goroutine也都會(huì)接收到同樣的錯(cuò)誤
到此這篇關(guān)于Go擴(kuò)展原語之SingleFlight的用法詳解的文章就介紹到這了,更多相關(guān)Go SingleFlight內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang性能提升利器之SectionReader的用法詳解
本文將介紹 Go 語言中的 SectionReader,包括 SectionReader的基本使用方法、實(shí)現(xiàn)原理、使用注意事項(xiàng),感興趣的小伙伴可以了解一下2023-07-07
golang調(diào)用shell命令(實(shí)時(shí)輸出,終止)
本文主要介紹了golang調(diào)用shell命令(實(shí)時(shí)輸出,終止),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
VsCode下開發(fā)Go語言的環(huán)境配置超詳細(xì)圖文詳解
vscode是一款跨平臺(tái)、輕量級(jí)、插件多的開源IDE,在vscode不僅可以配置C/C++、Python、R、Ruby等語言的環(huán)境,還可以配置Go語言的環(huán)境,下面這篇文章主要給大家介紹了關(guān)于VsCode下開發(fā)Go語言的環(huán)境配置,需要的朋友可以參考下2024-03-03
一文帶你了解Golang中reflect反射的常見錯(cuò)誤
go?反射的錯(cuò)誤大多數(shù)都來自于調(diào)用了一個(gè)不適合當(dāng)前類型的方法,?而且,這些錯(cuò)誤通常是在運(yùn)行時(shí)才會(huì)暴露出來,而不是在編譯時(shí),如果我們傳遞的類型在反射代碼中沒有被覆蓋到那么很容易就會(huì)?panic。本文就介紹一下使用?go?反射時(shí)很大概率會(huì)出現(xiàn)的錯(cuò)誤,需要的可以參考一下2023-01-01
beego獲取ajax數(shù)據(jù)的實(shí)例
下面小編就為大家分享一篇beego獲取ajax數(shù)據(jù)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2017-12-12

