簡介
看到一個有意思的庫:
SingleFlight是Go語言提供的一個擴充套件包。作用是當有多個goroutine同時呼叫同一個函式的時候,只允許一個goroutine去呼叫這個函式,等到這個呼叫的goroutine返回結果的時候,再把結果返回給這幾個同時呼叫的goroutine,這樣可以減少併發呼叫的數量。
Singleflight是以阻塞讀的方式來控制向下遊請求的併發量的,在第一個goroutine請求沒有返回前,所有的請求都將被阻塞。極端情況下可能導致我們的程式hang住,由於連鎖反應甚至導致我們整個系統掛掉。
可以傳遞ctx,制定超時時間,避免執行緒的長時間阻塞。
假設第一個執行緒超時了,後續只要有結果還是可以正確返回值的。
實現原理
Singleflight使用互斥鎖Muext和Map來實現,Mutext用來保證併發時的讀寫安全,Map用來儲存同一個key的正在處理(in flight)的請求。
type Group struct { mu sync.Mutex // protects m m map[string]*call // lazily initialized }
同時,Singleflight定義了一個call物件,call代表了正在執行的fn函式的請求或者是已經執行完成的請求。
// call is an in-flight or completed singleflight.Do call type call struct { wg sync.WaitGroup // These fields are written once before the WaitGroup is done // and are only read after the WaitGroup is done. val interface{} err error // These fields are read and written with the singleflight // mutex held before the WaitGroup is done, and are read but // not written after the WaitGroup is done. dups int chans []chan<- Result }
主要看下Do方法,DoChan方法和Do方法類似
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 { // 存在相同的key c.dups++ g.mu.Unlock() c.wg.Wait() // 等待這個key的第一個呼叫完成 // ... return c.val, c.err, true // 複用第一個key的請求結果 } c := new(call) // 第一個呼叫建立一個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 }
// doCall方法會實際的執行函式fn func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) { defer func() { // ... g.mu.Lock() defer g.mu.Unlock() c.wg.Done() // 阻塞等待完成 if g.m[key] == c { delete(g.m, key) // 呼叫完成刪除這個key } // ... }() func() { // ... c.val, c.err = fn() // 真正呼叫,結果賦值給call // ... }() }
應用場景
在快取擊穿的時候有用,假設大量的請求進來快取miss,請求擊穿到資料庫
有的資料當時是熱點,可能很快就變成了冷資料。針對類似的場景是不太合適設定不過期的,這個時候的Singleflight就派上用場了,可以避免大量的請求擊穿到資料庫,從而保護我們的服務穩定。
虛擬碼實現:
func getDataSingleFlight(key string) (interface{}, error) { v, err, _ := g.Do(key, func() (interface{}, error) { // 查快取 data, err := getDataFromCache(key) if err == nil { return data, nil } if errors.Is(err, ErrNotFound) { // 查DB data, err := getDataFromDB(key) if err == nil { setCache(data) // 設定快取 return data, nil } return nil, err } return nil, err // 快取出錯直接返回,防止穿透到DB }) if err != nil { return nil, err } return v, nil }