SingleFlight的作用是在處理多個goroutine同時調(diào)用同一個函數(shù)的時候椎侠,只讓一個goroutine去實(shí)際調(diào)用這個函數(shù),等到這個goroutine返回結(jié)果的時候措拇,再把結(jié)果返回給其他幾個同時調(diào)用了相同函數(shù)的goroutine我纪,這樣可以減少并發(fā)調(diào)用的數(shù)量。在實(shí)際應(yīng)用中也是丐吓,它能夠在一個服務(wù)中減少對下游的并發(fā)重復(fù)請求浅悉。還有一個比較常見的使用場景是用來防止緩存擊穿。
Go擴(kuò)展庫里用singleflight.Group結(jié)構(gòu)體類型提供了SingleFlight并發(fā)原語的功能券犁。
singleflight.Group類型提供了三個方法:
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result
func (g *Group) Forget(key string)
Do方法术健,接受一個字符串Key和一個待調(diào)用的函數(shù),會返回調(diào)用函數(shù)的結(jié)果和錯誤粘衬。使用Do方法的時候荞估,它會根據(jù)提供的Key判斷是否去真正調(diào)用fn函數(shù)咳促。同一個 key,在同一時間只有第一次調(diào)用Do方法時才會去執(zhí)行fn函數(shù)勘伺,其他并發(fā)的請求會等待調(diào)用的執(zhí)行結(jié)果跪腹。
DoChan方法:類似Do方法,只不過是一個異步調(diào)用飞醉。它會返回一個通道冲茸,等fn函數(shù)執(zhí)行完,產(chǎn)生了結(jié)果以后缅帘,就能從這個 chan 中接收這個結(jié)果轴术。
Forget方法:在SingleFlight中刪除一個Key。這樣一來钦无,之后這個Key的Do方法調(diào)用會執(zhí)行fn函數(shù)逗栽,而不是等待前一個未完成的fn 函數(shù)的結(jié)果。
使用緩存時铃诬,一個常見的用法是查詢一個數(shù)據(jù)先去查詢緩存祭陷,如果沒有就去數(shù)據(jù)庫里查到數(shù)據(jù)并緩存到Redis里。緩存擊穿問題是指趣席,高并發(fā)的系統(tǒng)中,大量的請求同時查詢一個緩存Key 時醇蝴,如果這個 Key 正好過期失效宣肚,就會導(dǎo)致大量的請求都打到數(shù)據(jù)庫上,這就是緩存擊穿悠栓。用 SingleFlight 來解決緩存擊穿問題再合適不過霉涨,這個時候只要這些對同一個 Key 的并發(fā)請求的其中一個到數(shù)據(jù)庫中查詢就可以了,這些并發(fā)的請求可以共享同一個結(jié)果惭适。
下面是一個模擬用SingleFlight并發(fā)原語合并查詢Redis緩存的程序笙瑟,觀察一下運(yùn)行的返回結(jié)果就會發(fā)現(xiàn)最終只執(zhí)行了一次Redis查詢。
package main
import (
"fmt"
"golang.org/x/sync/singleflight"
"sync"
"time"
)
// 模擬一個Redis客戶端
type client struct {
// ... 其他的配置省略
requestGroup singleflight.Group
}
// 普通查詢
func (c *client) Get(key string) (interface{}, error) {
fmt.Println("Querying Database")
time.Sleep(time.Second)
v := "Content of key" + key
return v, nil
}
// SingleFlight查詢
func (c *client) SingleFlightGet(key string) (interface{}, error) {
v, err, _ := c.requestGroup.Do(key, func() (interface{}, error) {
return c.Get(key)
})
if err != nil {
return nil, err
}
return v, err
}
func main() {
redisClient := new(client)
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
v, _ := redisClient.SingleFlightGet("Cyberpunk2077!!!")
fmt.Println(v)
}()
}
wg.Wait()
}