通過(guò) SingleFlight 模式學(xué)習(xí) Go 并發(fā)編程

最近接觸到微服務(wù)框架go-zero暮屡,翻看了整個(gè)框架代碼,發(fā)現(xiàn)結(jié)構(gòu)清晰毅桃、代碼簡(jiǎn)潔褒纲,所以決定閱讀源碼學(xué)習(xí)下准夷,本次閱讀的源碼位于core/syncx/singleflight.go

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

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

  1. 查詢緩存時(shí),合并請(qǐng)求彻秆,提升服務(wù)性能楔绞。
    假設(shè)有一個(gè) IP 查詢的服務(wù)左驾,每次用戶請(qǐng)求先在緩存中查詢一個(gè) IP 的歸屬地倡鲸,如果緩存中有結(jié)果則直接返回规揪,不存在則進(jìn)行 IP 解析操作鸵贬。

[圖片上傳失敗...(image-25bdd-1650858920151)]

如上圖所示次员,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)求合并的匙铡,效果如下:

[圖片上傳失敗...(image-2a7773-1650858920151)]

  1. 防止緩存擊穿。

緩存擊穿問(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ù)載上升。

[圖片上傳失敗...(image-d609c5-1650858920151)]

通過(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)求就阻塞茁驮拧(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))赋秀。
[圖片上傳失敗...(image-e3689a-1650858920151)]

從上圖可知利朵,其實(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.WaitGroupWait()方法實(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

歡迎使用 go-zerostar 支持我們售淡!

微信交流群

關(guān)注『微服務(wù)實(shí)踐』公眾號(hào)并點(diǎn)擊 交流群 獲取社區(qū)群二維碼。

如果你有 go-zero 的使用心得文章慷垮,或者源碼學(xué)習(xí)筆記揖闸,歡迎通過(guò)公眾號(hào)聯(lián)系投稿!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末料身,一起剝皮案震驚了整個(gè)濱河市汤纸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芹血,老刑警劉巖贮泞,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異幔烛,居然都是意外死亡啃擦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)饿悬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)令蛉,“玉大人,你說(shuō)我怎么就攤上這事乡恕⊙匝” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵傲宜,是天一觀的道長(zhǎng)运杭。 經(jīng)常有香客問(wèn)我,道長(zhǎng)函卒,這世上最難降的妖魔是什么辆憔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上虱咧,老公的妹妹穿的比我還像新娘熊榛。我一直安慰自己,他們只是感情好腕巡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布玄坦。 她就那樣靜靜地躺著,像睡著了一般绘沉。 火紅的嫁衣襯著肌膚如雪煎楣。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天车伞,我揣著相機(jī)與錄音择懂,去河邊找鬼。 笑死另玖,一個(gè)胖子當(dāng)著我的面吹牛困曙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谦去,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼慷丽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了哪轿?” 一聲冷哼從身側(cè)響起盈魁,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窃诉,沒(méi)想到半個(gè)月后杨耙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡飘痛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年珊膜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宣脉。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡车柠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出塑猖,到底是詐尸還是另有隱情竹祷,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布羊苟,位于F島的核電站塑陵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蜡励。R本人自食惡果不足惜令花,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一阻桅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兼都,春花似錦嫂沉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至芬萍,卻和暖如春尤揣,著一層夾襖步出監(jiān)牢的瞬間搔啊,已是汗流浹背柬祠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留负芋,地道東北人漫蛔。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像旧蛾,于是被迫代替她去往敵國(guó)和親莽龟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 本文從上下文Context锨天、同步原語(yǔ)與鎖毯盈、Channel、調(diào)度器四個(gè)方面介紹Go語(yǔ)言是如何實(shí)現(xiàn)并發(fā)的病袄。本文絕大部分...
    彥幀閱讀 1,558評(píng)論 1 3
  • 使用go1.10版本搂赋,在liteIde里開(kāi)發(fā)。 1益缠,變量聲明后必須使用脑奠,不然編譯不過(guò)(全局變量可以不用)。 2幅慌,變...
    adrian920閱讀 974評(píng)論 1 1
  • 能力模式 選擇題 【初級(jí)】下面屬于關(guān)鍵字的是()A. funcB. defC. structD. class 參考...
    靈魂深靈閱讀 5,281評(píng)論 2 5
  • go并發(fā)編程入門(mén)到放棄 并發(fā)和并行 并發(fā):一個(gè)處理器同時(shí)處理多個(gè)任務(wù)宋欺。 并行:多個(gè)處理器或者是多核的處理器同時(shí)處理...
    yangyunfeng閱讀 551評(píng)論 0 2
  • Chapter 8 Goroutines and Channels Go enable two styles of...
    SongLiang閱讀 1,574評(píng)論 0 3