golang使用sync.singleflight解決熱點(diǎn)緩存穿透問題
在 go
的 sync
包中,有一個(gè) singleflight
包,里面有一個(gè) singleflight.go
文件,代碼加注釋,一共 200 行出頭。內(nèi)容包括以下幾塊兒:
Group
結(jié)構(gòu)體管理一組相關(guān)的函數(shù)調(diào)用工作,它包含一個(gè)互斥鎖和一個(gè)map
,map
的key
是函數(shù)的名稱,value
是對應(yīng)的call
結(jié)構(gòu)體。call
結(jié)構(gòu)體表示一個(gè)inflight
或已完成的函數(shù)調(diào)用,包含等待組件WaitGroup
、調(diào)用結(jié)果val
和err
、調(diào)用次數(shù)dups
和通知通道chans
。Do
方法接收一個(gè)key
和函數(shù)fn
,它會先查看map
中是否已經(jīng)有這個(gè)key
的調(diào)用在inflight
,如果有則等待并返回已有結(jié)果,如果沒有則新建一個(gè)call
并執(zhí)行函數(shù)調(diào)用。DoChan
類似Do
但返回一個(gè)channel
來接收結(jié)果。doCall
方法包含了具體處理調(diào)用的邏輯,它會在函數(shù)調(diào)用前后添加defer
來recover
panic
和區(qū)分正常return
與runtime.Goexit
。- 如果發(fā)生
panic
,會將panicwraps
成錯(cuò)誤返回給等待的channel
,如果是goexit
會直接退出。正常return
時(shí)會將結(jié)果發(fā)送到所有通知channel
。 Forget
方法可以忘記一個(gè)key
的調(diào)用,下次Do
時(shí)會重新執(zhí)行函數(shù)。
這個(gè)包通過互斥鎖和 map
實(shí)現(xiàn)了對相同 key
的函數(shù)調(diào)用去重,可以避免對已有調(diào)用的重復(fù)計(jì)算,同時(shí)通過 channel
機(jī)制可以通知調(diào)用者函數(shù)執(zhí)行結(jié)果。在一些需要確保單次執(zhí)行的場景中,可以使用這個(gè)包中的方法。
通過 singleflight
可以很容易實(shí)現(xiàn)緩存和去重的效果,避免重復(fù)計(jì)算,接下來,我們來模擬一下并發(fā)請求可能導(dǎo)致的緩存穿透場景,以及如何用 singleflight
包來解決這個(gè)問題:
package main import ( "context" "fmt" "golang.org/x/sync/singleflight" "sync/atomic" "time" ) type Result string // 模擬查詢數(shù)據(jù)庫 func find(ctx context.Context, query string) (Result, error) { return Result(fmt.Sprintf("result for %q", query)), nil } func main() { var g singleflight.Group const n = 200 waited := int32(n) done := make(chan struct{}) key := "this is key" for i := 0; i < n; i++ { go func(j int) { v, _, shared := g.Do(key, func() (interface{}, error) { ret, err := find(context.Background(), key) return ret, err }) if atomic.AddInt32(&waited, -1) == 0 { close(done) } fmt.Printf("index: %d, val: %v, shared: %v\n", j, v, shared) }(i) } select { case <-done: case <-time.After(time.Second): fmt.Println("Do hangs") } time.Sleep(time.Second * 4) }
在這段程序中,如果重復(fù)使用查詢結(jié)果,shared
會返回 true
,穿透查詢會返回 false
上面的設(shè)計(jì)中還有一個(gè)問題,就是在 Do 阻塞時(shí),所有請求都會阻塞,內(nèi)存可能會出現(xiàn)大的問題。
此時(shí),Do
可以更換為DoChan
,兩者實(shí)現(xiàn)上完全一樣,不同的是,DoChan()
通過 channel
返回結(jié)果。因此可以使用 select
語句實(shí)現(xiàn)超時(shí)控制
ch := g.DoChan(key, func() (interface{}, error) { ret, err := find(context.Background(), key) return ret, err }) // Create our timeout timeout := time.After(500 * time.Millisecond) var ret singleflight.Result select { case <-timeout: // Timeout elapsed fmt.Println("Timeout") return case ret = <-ch: // Received result from channel fmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared) }
在超時(shí)時(shí)主動返回,不阻塞。
此時(shí)又引入了另一個(gè)問題,這樣的每一次的請求,并不是高可用的,成功率是無法保證的。這時(shí)候可以增加一定的請求飽和度來保證業(yè)務(wù)的最終成功率,此時(shí)一次請求還是多次請求,對于下游服務(wù)而言并沒有太大區(qū)別,此時(shí)使用 singleflight
只是為了降低請求的數(shù)量級,那么可以使用 Forget()
來提高下游請求的并發(fā)。
ch := g.DoChan(key, func() (interface{}, error) { go func() { time.Sleep(10 * time.Millisecond) fmt.Printf("Deleting key: %v\n", key) g.Forget(key) }() ret, err := find(context.Background(), key) return ret, err })
當(dāng)然,這種做法依然無法保證100%的成功,如果單次的失敗無法容忍,在高并發(fā)的場景下需要使用更好的處理方案,比如犧牲一部分實(shí)時(shí)性、完全使用緩存查詢 + 異步更新等。
到此這篇關(guān)于golang使用sync.singleflight解決熱點(diǎn)緩存穿透問題的文章就介紹到這了,更多相關(guān)golang sync.singleflight緩存穿透內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go使用支付寶沙箱實(shí)現(xiàn)支付寶支付的操作步驟
支付寶沙箱支付是支付寶提供的一個(gè)測試環(huán)境,用于開發(fā)者在不影響真實(shí)交易的情況下進(jìn)行支付接口的開發(fā)和調(diào)試,本文給大家介紹了go使用支付寶沙箱實(shí)現(xiàn)支付寶支付的操作步驟,文中有詳細(xì)的代碼示例和圖文供大家參考,需要的朋友可以參考下2024-03-03Go項(xiàng)目在linux服務(wù)器的部署詳細(xì)步驟
在今天的軟件開發(fā)中,使用Linux作為操作系統(tǒng)的比例越來越高,而Golang語言則因?yàn)槠涓咝?、簡潔和并發(fā)性能等特點(diǎn),也被越來越多的開發(fā)者所青睞,這篇文章主要給大家介紹了關(guān)于Go項(xiàng)目在linux服務(wù)器的部署詳細(xì)步驟,需要的朋友可以參考下2023-09-09Go Struct結(jié)構(gòu)體的具體實(shí)現(xiàn)
Go語言中通過結(jié)構(gòu)體的內(nèi)嵌再配合接口比面向?qū)ο缶哂懈叩臄U(kuò)展性和靈活性,本文主要介紹了Go Struct結(jié)構(gòu)體的具體實(shí)現(xiàn),感興趣的可以了解一下2023-03-03go?micro微服務(wù)proto開發(fā)安裝及使用規(guī)則
這篇文章主要為大家介紹了go?micro微服務(wù)proto開發(fā)中安裝Protobuf及基本規(guī)范字段的規(guī)則詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解
這篇文章主要為大家詳細(xì)介紹了Go語言數(shù)據(jù)結(jié)構(gòu)中二叉樹可視化的方法詳解,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-09-09golang開啟mod后import報(bào)紅的簡單解決方案
這篇文章主要給大家介紹了關(guān)于golang開啟mod后import報(bào)紅的簡單解決方案,文中通過圖文將解決的辦法介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-01-01go語言實(shí)現(xiàn)的memcache協(xié)議服務(wù)的方法
這篇文章主要介紹了go語言實(shí)現(xiàn)的memcache協(xié)議服務(wù)的方法,實(shí)例分析了Go語言使用memcache的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03Go語言設(shè)計(jì)模式之結(jié)構(gòu)型模式
本文主要聚焦在結(jié)構(gòu)型模式(Structural Pattern)上,其主要思想是將多個(gè)對象組裝成較大的結(jié)構(gòu),并同時(shí)保持結(jié)構(gòu)的靈活和高效,從程序的結(jié)構(gòu)上解決模塊之間的耦合問題2021-06-06