go-zero 微服務框架中提供了許多開箱即用的工具,好的工具不只能提高服務的性能並且還能提高代碼的魯棒性避免出錯,實現代碼風格的統一方便他人閱讀等等。git
本文主要講述進程內共享調用神器 SharedCallsgithub
使用場景
併發場景下,可能會有多個線程(協程)同時請求同一份資源,若是每一個請求都要走一遍資源的請求過程,除了比較低效以外,還會對資源服務形成併發的壓力。舉一個具體例子,好比緩存失效,多個請求同時到達某服務請求某資源,該資源在緩存中已經失效,此時這些請求會繼續訪問DB作查詢,會引發數據庫壓力瞬間增大。而使用 SharedCalls 可使得同時多個請求只須要發起一次拿結果的調用,其餘請求"不勞而獲",這種設計有效減小了資源服務的併發壓力,能夠有效防止緩存擊穿。sql
高併發場景下,當某個熱點 key 緩存失效後,多個請求會同時從數據庫加載該資源,並保存到緩存,若是不作防範,可能會致使數據庫被直接打死。針對這種場景,go-zero 框架中已經提供了實現,具體可參看 sqlc 和 mongoc 等實現代碼。數據庫
爲了簡化演示代碼,咱們經過多個線程同時去獲取一個 id 來模擬緩存的場景。以下:緩存
func main() { const round = 5 var wg sync.WaitGroup barrier := syncx.NewSharedCalls() wg.Add(round) for i := 0; i < round; i++ { // 多個線程同時執行 go func() { defer wg.Done() // 能夠看到,多個線程在同一個 key 上去請求資源,獲取資源的實際函數只會被調用一次 val, err := barrier.Do("once", func() (interface{}, error) { // sleep 1秒,爲了讓多個線程同時取 once 這個 key 上的數據 time.Sleep(time.Second) // 生成了一個隨機的 id return stringx.RandId(), nil }) if err != nil { fmt.Println(err) } else { fmt.Println(val) } }() } wg.Wait() }
運行,打印結果爲:微信
837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db 837c577b1008a0db
能夠看出,只要是同一個 key 上的同時發起的請求,都會共享同一個結果,對獲取 DB 數據進緩存等場景特別有用,能夠有效防止緩存擊穿。併發
關鍵源碼分析
-
SharedCalls interface 提供了 Do 和 DoEx 兩種方法的抽象框架
type SharedCalls interface { Do(key string, fn func() (interface{}, error)) (interface{}, error) DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error) }
-
SharedCalls interface 的具體實現 sharedGroup函數
// call 表明對指定資源的一次請求 type call struct { wg sync.WaitGroup // 用於協調各個請求 goroutine 之間的資源共享 val interface{} // 用於保存請求的返回值 err error // 用於保存請求過程當中發生的錯誤 } type sharedGroup struct { calls map[string]*call lock sync.Mutex }
-
sharedGroup 的 Do 方法微服務
- key 參數:能夠理解爲資源的惟一標識。
- fn 參數:真正獲取資源的方法。
- 處理過程分析:
// 當多個請求同時使用 Do 方法請求資源時 func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) { // 先申請加鎖 g.lock.Lock() // 根據 key,獲取對應的 call 結果,並用變量 c 保存 if c, ok := g.calls[key]; ok { // 拿到 call 之後,釋放鎖,此處 call 可能尚未實際數據,只是一個空的內存佔位 g.lock.Unlock() // 調用 wg.Wait,判斷是否有其餘 goroutine 正在申請資源,若是阻塞,說明有其餘 goroutine 正在獲取資源 c.wg.Wait() // 當 wg.Wait 再也不阻塞,表示資源獲取已經結束,能夠直接返回結果 return c.val, c.err } // 沒有拿到結果,則調用 makeCall 方法去獲取資源,注意此處仍然是鎖住的,能夠保證只有一個 goroutine 能夠調用 makecall c := g.makeCall(key, fn) // 返回調用結果 return c.val, c.err }
-
sharedGroup 的 DoEx 方法
- 和 Do 方法相似,只是返回值中增長了布爾值表示值是調用 makeCall 方法直接獲取的,仍是取的共享成果
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) { g.lock.Lock() if c, ok := g.calls[key]; ok { g.lock.Unlock() c.wg.Wait() return c.val, false, c.err } c := g.makeCall(key, fn) return c.val, true, c.err }
-
sharedGroup 的 makeCall 方法
- 該方法由 Do 和 DoEx 方法調用,是真正發起資源請求的方法。
// 進入 makeCall 的必定只有一個 goroutine,由於要拿鎖鎖住的 func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call { // 建立 call 結構,用於保存本次請求的結果 c := new(call) // wg 加 1,用於通知其餘請求資源的 goroutine 等待本次資源獲取的結束 c.wg.Add(1) // 將用於保存結果的 call 放入 map 中,以供其餘 goroutine 獲取 g.calls[key] = c // 釋放鎖,這樣其餘請求的 goroutine 才能獲取 call 的內存佔位 g.lock.Unlock() defer func() { // delete key first, done later. can't reverse the order, because if reverse, // another Do call might wg.Wait() without get notified with wg.Done() g.lock.Lock() delete(g.calls, key) g.lock.Unlock() // 調用 wg.Done,通知其餘 goroutine 能夠返回結果,這樣本批次全部請求完成結果的共享 c.wg.Done() }() // 調用 fn 方法,將結果填入變量 c 中 c.val, c.err = fn() return c }
總結
本文主要介紹了 go-zero 框架中的 SharedCalls 工具,對其應用場景和關鍵代碼作了簡單的梳理,但願本篇文章能給你們帶來一些收穫。
項目地址
https://github.com/tal-tech/go-zero