長(zhǎng)沙網(wǎng)站建設(shè) 鼎譽(yù)百度瀏覽器官方下載
前言
在緩存系統(tǒng)中,如果發(fā)生了緩存未命中,通常會(huì)向數(shù)據(jù)庫(kù)或者其他的緩存系統(tǒng)來(lái)請(qǐng)求數(shù)據(jù)。
想象這樣一種情況,緩存系統(tǒng)中某個(gè)熱點(diǎn)值被刪除了,隨后一大批請(qǐng)求到來(lái),造成大量的cache miss
,如果這些請(qǐng)求全部都轉(zhuǎn)向DB,那么會(huì)造成DB請(qǐng)求量大,壓力增加,這就是典型的緩存擊穿。
那么如何避免緩存擊穿這種情況呢?
通常有如下幾種辦法:
-
設(shè)置永不過(guò)期的緩存:對(duì)于一些特別熱門的數(shù)據(jù)可以設(shè)置成永不過(guò)期,這樣可以避免由于緩存過(guò)期而導(dǎo)致的緩存擊穿。
-
緩存預(yù)熱:在系統(tǒng)啟動(dòng)或者是緩存過(guò)期之前,可以把可能會(huì)被訪問(wèn)到的數(shù)據(jù)提前加載到緩存中,這樣可以避免緩存過(guò)期導(dǎo)致的緩存擊穿。
-
使用singlefight將多個(gè)對(duì)數(shù)據(jù)庫(kù)的請(qǐng)求轉(zhuǎn)變成一個(gè)。
本文著眼于singlefight方法,接下來(lái)會(huì)講解singlefight的原理及實(shí)現(xiàn)。
singlefight原理及實(shí)現(xiàn)
原理
singleflight是一種讓相同的請(qǐng)求在短時(shí)間內(nèi)的全局范圍內(nèi)只執(zhí)行一次的機(jī)制。即,當(dāng)多個(gè)goroutine并發(fā)的調(diào)用相同請(qǐng)求時(shí),只有一個(gè)請(qǐng)求會(huì)被執(zhí)行,而其他的請(qǐng)求會(huì)等待第一個(gè)請(qǐng)求執(zhí)行結(jié)束后,再返回執(zhí)行結(jié)果。
此算法的核心就是使用一個(gè)map結(jié)構(gòu)來(lái)保存key與函數(shù)運(yùn)行狀態(tài)的映射,在每次調(diào)用函數(shù)的時(shí)候,檢查key是否在map中,如果存在,說(shuō)明已經(jīng)有相同請(qǐng)求的函數(shù)正在調(diào)用了,就不會(huì)額外的運(yùn)行函數(shù),而是等待之前函數(shù)運(yùn)行的結(jié)果;如果不存在,就會(huì)運(yùn)行函數(shù)。
實(shí)現(xiàn)
定義Call保存函數(shù)運(yùn)行狀態(tài):
type Call struct {wg sync.WaitGroupval interface{}err error
}
定義Group用于與用戶交互,也是singleflight的核心結(jié)構(gòu)體,map用于存儲(chǔ)key與函數(shù)運(yùn)行狀態(tài)的映射,鎖用于保護(hù)map值的并發(fā)修改:
type Group struct {mu sync.Mutexm map[string]*Call
}
為Group編寫Do方法,核心邏輯就是判斷key在map中是否存在,如果存在就會(huì)等待函數(shù)運(yùn)行完畢后返回(通過(guò)waitgroup),如果不存在則會(huì)調(diào)用fn函數(shù),注意要用鎖保護(hù)map的并發(fā)訪問(wèn):
func (g *Group) Do(key string,fn func()(interface{},error))(interface{},error) {g.mu.Lock()//懶加載if g.m == nil{g.m = make(map[string]*Call)}if v,ok:=g.m[key];ok{g.mu.Unlock()v.wg.Wait()return v.val,v.err}else {c := &Call{}c.wg.Add(1)g.m[key] = cg.mu.Unlock()c.val,c.err = fn()c.wg.Done()g.mu.Lock()delete(g.m,key)g.mu.Unlock()return c.val,c.err}}
編寫測(cè)試代碼進(jìn)行測(cè)試吧:
func TestGroup_Do(t *testing.T) {var wg sync.WaitGroupg := &Group{}var num int//模仿Db函數(shù)getDb := func(i int,key string)[]byte {log.Println(i,"search the Db for",key)num++time.Sleep(1*time.Second)return []byte(key)}key := "key"for i := 0; i < 10; i++ {go func(i int) {wg.Add(1)v,_:=g.Do(key, func() (interface{}, error) {return getDb(i,key),nil})log.Println(i,"get the data :",v)wg.Done()}(i)}time.Sleep(2*time.Second)for i := 10; i < 20; i++ {go func(i int) {wg.Add(1)v,_:=g.Do(key, func() (interface{}, error) {return getDb(i,key),nil})log.Println(i,"get the data :",v)wg.Done()}(i)}wg.Wait()if num > 2 {log.Fatal("wrong!there are two many getDb!")}
}
測(cè)試結(jié)果并發(fā)接受到多個(gè)請(qǐng)求時(shí),只會(huì)運(yùn)行一個(gè)請(qǐng)求。
Sync.Once和SingleFlight對(duì)比
Sync.Once和SingleFlight的功能比較像,但是也有一些區(qū)別:
sync.Once
用于確保某個(gè)函數(shù)在多線程環(huán)境下只被執(zhí)行一次。當(dāng)多個(gè)線程同時(shí)調(diào)用 Do()
方法時(shí),只有第一個(gè)調(diào)用的線程會(huì)執(zhí)行函數(shù),其他線程會(huì)被阻塞等待,直到第一個(gè)線程完成。這個(gè)工具通常用于執(zhí)行初始化操作,確保全局只會(huì)初始化一次。
singleflight
則是用于減少重復(fù)調(diào)用相同函數(shù)的次數(shù)。當(dāng)多個(gè)線程同時(shí)調(diào)用 Do()
方法,如果相同的參數(shù)已經(jīng)在處理中,那么后續(xù)的調(diào)用會(huì)被阻塞等待,直到處理完成后才返回相同的結(jié)果。如果參數(shù)不同,那么會(huì)啟動(dòng)新的處理流程。這個(gè)工具通常用于避免重復(fù)計(jì)算或者重復(fù)查詢數(shù)據(jù)庫(kù)等場(chǎng)景。
總的來(lái)說(shuō),sync.Once
主要用于保證某個(gè)函數(shù)只會(huì)被執(zhí)行一次通常用于初始化中,而 singleflight
主要用于避免重復(fù)計(jì)算和查詢通常用于數(shù)據(jù)庫(kù)中。如保證緩存擊穿。