日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
通過(guò)SingleFlight模式學(xué)習(xí)Go并發(fā)編程

在 go-zero 中 SingleFlight 的作用是: 將并發(fā)請(qǐng)求合并成一個(gè)請(qǐng)求,以減少對(duì)下層服務(wù)的壓力。

為朝陽(yáng)等地區(qū)用戶提供了全套網(wǎng)頁(yè)設(shè)計(jì)制作服務(wù),及朝陽(yáng)網(wǎng)站建設(shè)行業(yè)解決方案。主營(yíng)業(yè)務(wù)為成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作、朝陽(yáng)網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠(chéng)的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!

應(yīng)用場(chǎng)景

1.查詢緩存時(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)求合并的,效果如下:

2.防止緩存擊穿。

緩存擊穿問(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é)果如下:

從上看出,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)求就阻塞住(done 為 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)鍵就兩步:

  1. 判斷是第一個(gè)請(qǐng)求的協(xié)程(利用map)
  2. 阻塞住其他所有協(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),也是分成兩步:

  1. 刪除 map 中的 key,使得下次發(fā)起請(qǐng)求可以獲取新的值。
  2. 調(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


分享文章:通過(guò)SingleFlight模式學(xué)習(xí)Go并發(fā)編程
當(dāng)前鏈接:http://www.5511xx.com/article/dhcogci.html