防止緩存擊穿庫(kù):singleflight 源碼分析
singleflight通常被用來(lái)做防止緩存擊穿,代碼位置在https://github.com/golang/groupcache/tree/master/singleflight,在詳細(xì)介紹代碼內(nèi)容之前,我先區(qū)分下雪崩、穿透和擊穿:
雪崩
雪崩就是指緩存中大批量熱點(diǎn)數(shù)據(jù)同時(shí)過(guò)期或緩存機(jī)器意外發(fā)生了全盤(pán)宕機(jī)后系統(tǒng)涌入大量查詢請(qǐng)求,因?yàn)榇蟛糠謹(jǐn)?shù)據(jù)在Redis層已經(jīng)失效,請(qǐng)求滲透到數(shù)據(jù)庫(kù)層,大批量請(qǐng)求猶如洪水一般涌入,引起數(shù)據(jù)庫(kù)壓力造成查詢堵塞甚至宕機(jī)。
解決辦法:
將緩存失效時(shí)間分散開(kāi),比如每個(gè)key的過(guò)期時(shí)間是隨機(jī),防止同一時(shí)間大量數(shù)據(jù)過(guò)期現(xiàn)象發(fā)生,這樣不會(huì)出現(xiàn)同一時(shí)間全部請(qǐng)求都落在數(shù)據(jù)庫(kù)層,如果緩存數(shù)據(jù)庫(kù)是分布式部署,將熱點(diǎn)數(shù)據(jù)均勻分布在不同Redis和數(shù)據(jù)庫(kù)中,有效分擔(dān)壓力,別一個(gè)人扛。
簡(jiǎn)單粗暴,讓Redis數(shù)據(jù)永不過(guò)期(如果業(yè)務(wù)準(zhǔn)許,比如不用更新的名單類(lèi))。當(dāng)然,如果業(yè)務(wù)數(shù)據(jù)準(zhǔn)許的情況下可以,比如中獎(jiǎng)名單用戶,每期用戶開(kāi)獎(jiǎng)后,名單不可能會(huì)變了,無(wú)需更新。
事前:redis 高可用,主從+哨兵,redis cluster,避免全盤(pán)崩潰。- 事中:本地 ehcache 緩存 + hystrix 限流&降級(jí),避免 MySQL 被打死。- 事后:redis 持久化,一旦重啟,自動(dòng)從磁盤(pán)上加載數(shù)據(jù),快速恢復(fù)緩存數(shù)據(jù)。
緩存穿透
緩存穿透是指段時(shí)間涌入大量請(qǐng)求,緩存中查不到,每次你去數(shù)據(jù)庫(kù)里查,也查不到。(數(shù)據(jù)庫(kù) id 是從 1 開(kāi)始的,結(jié)果黑客發(fā)過(guò)來(lái)的請(qǐng)求 id 全部都是負(fù)數(shù)。)這樣的話,緩存中不會(huì)有,請(qǐng)求每次都“視緩存于無(wú)物”,直接查詢數(shù)據(jù)庫(kù)。這種惡意攻擊場(chǎng)景的緩存穿透就會(huì)直接把數(shù)據(jù)庫(kù)給打死。
解決方式很簡(jiǎn)單,每次系統(tǒng) A 從數(shù)據(jù)庫(kù)中只要沒(méi)查到,就寫(xiě)一個(gè)空值到緩存里去,比如 set -999 UNKNOWN。然后設(shè)置一個(gè)過(guò)期時(shí)間,這樣的話,下次有相同的 key 來(lái)訪問(wèn)的時(shí)候,在緩存失效之前,都可以直接從緩存中取數(shù)據(jù)。
緩存擊穿
緩存擊穿,某個(gè) key 非常熱點(diǎn),訪問(wèn)非常頻繁,處于集中式高并發(fā)訪問(wèn)的情況,當(dāng)這個(gè) key 在失效的瞬間,大量的請(qǐng)求就擊穿了緩存,直接請(qǐng)求數(shù)據(jù)庫(kù),就像是在一道屏障上鑿開(kāi)了一個(gè)洞。
方法一:我們簡(jiǎn)單粗暴點(diǎn),直接讓熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過(guò)期,定時(shí)任務(wù)定期去刷新數(shù)據(jù)就可以了。不過(guò)這樣設(shè)置需要區(qū)分場(chǎng)景,比如某寶首頁(yè)可以這么做。
方法二:為了避免出現(xiàn)緩存擊穿的情況,我們可以在第一個(gè)請(qǐng)求去查詢數(shù)據(jù)庫(kù)的時(shí)候?qū)λ右粋€(gè)互斥鎖,其余的查詢請(qǐng)求都會(huì)被阻塞住,直到鎖被釋放,后面的線程進(jìn)來(lái)發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存,從而保護(hù)數(shù)據(jù)庫(kù)。但是也是由于它會(huì)阻塞其他的線程,此時(shí)系統(tǒng)吞吐量會(huì)下降。需要結(jié)合實(shí)際的業(yè)務(wù)去考慮是否要這么做。
方法三:就是singleflight的設(shè)計(jì)思路,也會(huì)使用互斥鎖,但是相對(duì)于方法二的加鎖粒度會(huì)更細(xì)
singleflight 源碼分析
說(shuō)完了singleflight的應(yīng)用場(chǎng)景,下面詳細(xì)分析下singleflight的源碼,源碼非常簡(jiǎn)潔,目錄下就包含了兩個(gè)文件singleflight.go 和對(duì)應(yīng)的測(cè)試的測(cè)試文件singleflight_test.go
源碼中就定義了兩個(gè)結(jié)構(gòu)體和一個(gè)方法
// call is an in-flight or completed Do calltype call struct {wg sync.WaitGroupval interface{}err error}
// Group represents a class of work and forms a namespace in which// units of work can be executed with duplicate suppression.type Group struct {mu sync.Mutex // protects mm map[string]*call // lazily initialized}
通過(guò)call的waitGroup來(lái)阻塞相同key的請(qǐng)求,實(shí)現(xiàn)了一個(gè)指允許一個(gè)請(qǐng)求到后端,通過(guò)Group的m來(lái)實(shí)現(xiàn)相同key的數(shù)據(jù)共享,大家取同一份結(jié)果,下面看下Do函數(shù)的具體實(shí)現(xiàn):
// Do executes and returns the results of the given function, making// sure that only one execution is in-flight for a given key at a// time. If a duplicate comes in, the duplicate caller waits for the// original to complete and receives the same results.//同一個(gè)對(duì)象多次同時(shí)多次調(diào)用這個(gè)邏輯的時(shí)候,可以使用其中的一個(gè)去執(zhí)行func (g *Group) Do(key string, fn func()(interface{},error)) (interface{}, error ){g.mu.Lock() //加鎖保護(hù)存放key的map,因?yàn)橐l(fā)執(zhí)行if g.m == nil { //lazing make 方式建立g.m = make(map[string]*call)}if c, ok := g.m[key]; ok { //如果map中已經(jīng)存在對(duì)這個(gè)key的處理那就等著吧g.mu.Unlock() //解鎖,對(duì)map的操作已經(jīng)完畢c.wg.Wait()return c.val,c.err //map中只有一份key,所以只有一個(gè)c}c := new(call) //創(chuàng)建一個(gè)工作單元,只負(fù)責(zé)處理一種keyc.wg.Add(1)g.m[key] = c //將key注冊(cè)到map中g.mu.Unlock() //map的操做完成,解鎖c.val, c.err = fn()//第一個(gè)注冊(cè)者去執(zhí)行c.wg.Done()g.mu.Lock()delete(g.m,key) //對(duì)map進(jìn)行操作,需要枷鎖g.mu.Unlock()return c.val, c.err //給第一個(gè)注冊(cè)者返回結(jié)果}
執(zhí)行過(guò)程如下:
1,對(duì)于相同key的請(qǐng)求,大家搶鎖,只有第一個(gè)請(qǐng)求可以獲得鎖;
2,然后查詢map發(fā)現(xiàn)沒(méi)有數(shù)據(jù),創(chuàng)建一個(gè)call,waitGroup加1,寫(xiě)入map,然后釋放鎖;做到了鎖的粒度最小化。
3,其他獲得鎖的請(qǐng)求,從map中取到call,由于函數(shù)fn還沒(méi)有執(zhí)行完畢,所以waitGroup還在等待狀態(tài),后面獲得鎖的請(qǐng)求都在等待這個(gè)waitGroup;
4,當(dāng)函數(shù)執(zhí)行完畢以后,獲得了數(shù)據(jù),調(diào)用wg.Done()通知所有等待的請(qǐng)求獲取數(shù)據(jù),實(shí)現(xiàn)了大家共享一份數(shù)據(jù);
5,然后加鎖做清理工作,清理掉map里存儲(chǔ)的數(shù)據(jù)。
推薦閱讀
