Go語(yǔ)言使用singleflight解決緩存擊穿
前言
在構(gòu)建高性能的服務(wù)時(shí),緩存是優(yōu)化數(shù)據(jù)庫(kù)壓力和提高響應(yīng)速度的關(guān)鍵技術(shù)。使用緩存也會(huì)帶來(lái)一些問(wèn)題,其中就包括 緩存擊穿,它不僅會(huì)導(dǎo)致數(shù)據(jù)庫(kù)壓力劇增,引起數(shù)據(jù)庫(kù)性能的下降,嚴(yán)重時(shí)甚至?xí)艨鍞?shù)據(jù)庫(kù),導(dǎo)致數(shù)據(jù)庫(kù)不可用。
在 Go
語(yǔ)言中,golang.org/x/sync/singleflight
包提供了一種機(jī)制,確保對(duì)于任何特定 key
的并發(fā)請(qǐng)求在同一時(shí)刻只執(zhí)行一次。這個(gè)機(jī)制有效地防止了緩存擊穿問(wèn)題。
本文將深入探討 Go
語(yǔ)言中 singleflight
包的使用。從緩存擊穿問(wèn)題的基礎(chǔ)知識(shí)開(kāi)始,進(jìn)而詳細(xì)介紹 singleflight
包的使用,展示如何利用它來(lái)避免緩存擊穿。
準(zhǔn)備好了嗎?準(zhǔn)備一杯你最喜歡的咖啡或茶,隨著本文一探究竟吧。
緩存擊穿
緩存擊穿 是指在高并發(fā)的情況下,某個(gè)熱點(diǎn)的 key
突然過(guò)期,導(dǎo)致大量的請(qǐng)求直接訪問(wèn)數(shù)據(jù)庫(kù),造成數(shù)據(jù)庫(kù)的壓力過(guò)大,甚至宕機(jī)的現(xiàn)象。
緩存擊穿流程圖.png
常見(jiàn)的解決方案:
- 設(shè)置熱點(diǎn)數(shù)據(jù)永不過(guò)期:對(duì)于一些確定的熱點(diǎn)數(shù)據(jù),可以將其設(shè)置為 永不過(guò)期,這樣就可以確保不會(huì)因?yàn)榫彺媸Ф鴮?dǎo)致請(qǐng)求直接訪問(wèn)數(shù)據(jù)庫(kù)。
- 設(shè)置互斥鎖:為了防止緩存失效時(shí)所有請(qǐng)求同時(shí)查詢數(shù)據(jù)庫(kù),可以采用鎖機(jī)制確保僅有一個(gè)請(qǐng)求查詢數(shù)據(jù)庫(kù)并更新緩存,而其他請(qǐng)求則在緩存更新后再進(jìn)行訪問(wèn)。
- 提前更新:后臺(tái)監(jiān)控緩存的使用情況,當(dāng)緩存即將過(guò)期時(shí),異步更新緩存,延長(zhǎng)過(guò)期時(shí)間。
singleflight 包
Package singleflight provides a duplicate function call suppression mechanism.
這段英文來(lái)自官方文檔的介紹,直譯過(guò)來(lái)的意思是:singleflight
包提供了一種“重復(fù)函數(shù)調(diào)用抑制機(jī)制”。
換句話說(shuō),當(dāng)多個(gè) goroutine
同時(shí)嘗試調(diào)用同一個(gè)函數(shù)(基于某個(gè)給定的 key
)時(shí),singleflight
會(huì)確保該函數(shù)只會(huì)被第一個(gè)到達(dá)的 goroutine
調(diào)用,其他 goroutine
會(huì)等待這次調(diào)用的結(jié)果,然后共享這個(gè)結(jié)果,而不是同時(shí)發(fā)起多個(gè)調(diào)用。
一句話概括就是 singleflight
將多個(gè)請(qǐng)求合并成一個(gè)請(qǐng)求,多個(gè)請(qǐng)求共享同一個(gè)結(jié)果。
組成部分
Group
:這是 singleflight
包的核心結(jié)構(gòu)體。它管理著所有的請(qǐng)求,確保同一時(shí)刻,對(duì)同一資源的請(qǐng)求只會(huì)被執(zhí)行一次。Group
對(duì)象不需要顯式創(chuàng)建,直接聲明后即可使用。
Do
方法:Group
結(jié)構(gòu)體提供了 Do
方法,這是實(shí)現(xiàn)合并請(qǐng)求的主要方法,該方法接收兩個(gè)參數(shù):一個(gè)是字符串 key
(用于標(biāo)識(shí)請(qǐng)求資源),另一個(gè)是函數(shù) fn
,用來(lái)執(zhí)行實(shí)際的任務(wù)。在調(diào)用 Do
方法時(shí),如果已經(jīng)有一個(gè)相同 key
的請(qǐng)求正在執(zhí)行,那么 Do
方法會(huì)等待這個(gè)請(qǐng)求完成并共享結(jié)果,否則執(zhí)行 fn
函數(shù),然后返回結(jié)果。
Do
方法有三個(gè)返回值,前兩個(gè)返回值是 fn
函數(shù)的返回值,類型分別為 interface{}
和 error
,最后一個(gè)返回值是一個(gè) bool
類型,表示 Do
方法的返回結(jié)果是否被多個(gè)調(diào)用共享。
DoChan
:該方法與 Do
方法類似,但它返回的是一個(gè)通道,通道在操作完成時(shí)接收到結(jié)果。返回值是通道,意味著我們能以非阻塞的方式等待結(jié)果。
Forget
:該方法用于從 Group
中刪除一個(gè) key
以及相關(guān)的請(qǐng)求記錄,確保下次用同一 key
調(diào)用 Do
時(shí),將立即執(zhí)行新請(qǐng)求,而不是復(fù)用之前的結(jié)果。
Result
:這是 DoChan
方法返回結(jié)果時(shí)所使用的結(jié)構(gòu)體類型,用于封裝請(qǐng)求的結(jié)果。這個(gè)結(jié)構(gòu)體包含三個(gè)字段,具體如下:
Val
(interface{}
類型):請(qǐng)求返回的結(jié)果。Err
(error
類型):請(qǐng)求過(guò)程中發(fā)生的錯(cuò)誤信息。Shared
(bool
類型):表示這個(gè)結(jié)果是否被當(dāng)前請(qǐng)求以外的其他請(qǐng)求共享。
安裝
通過(guò)以下命令,在 go
應(yīng)用中安裝 singleflight
依賴:
go get golang.org/x/sync/singleflight
使用示例
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/singleflight/usage/main.go package main import ( "errors" "fmt" "golang.org/x/sync/singleflight" "sync" ) var errRedisKeyNotFound = errors.New("redis: key not found") func fetchDataFromCache() (any, error) { fmt.Println("fetch data from cache") returnnil, errRedisKeyNotFound } func fetchDataFromDataBase() (any, error) { fmt.Println("fetch data from database") return"程序員陳明勇", nil } func fetchData() (any, error) { cache, err := fetchDataFromCache() if err != nil && errors.Is(err, errRedisKeyNotFound) { fmt.Println(errRedisKeyNotFound.Error()) return fetchDataFromDataBase() } return cache, err } func main() { var ( sg singleflight.Group wg sync.WaitGroup ) forrange5 { wg.Add(1) gofunc() { defer wg.Done() v, err, shared := sg.Do("key", fetchData) if err != nil { panic(err) } fmt.Printf("v: %v, shared: %v\n", v, shared) }() } wg.Wait() }
singleflight.png
這段代碼模擬了一個(gè)典型的并發(fā)訪問(wèn)場(chǎng)景:從緩存獲取數(shù)據(jù),若緩存未命中,則從數(shù)據(jù)庫(kù)檢索。在此過(guò)程中,singleflight
庫(kù)起到了至關(guān)重要的作用。它確保在多個(gè)并發(fā)請(qǐng)求嘗試同時(shí)獲取相同數(shù)據(jù)時(shí),實(shí)際的獲取操作(不論是訪問(wèn)緩存還是查詢數(shù)據(jù)庫(kù))只會(huì)執(zhí)行一次。這樣不僅減輕了數(shù)據(jù)庫(kù)的壓力,還有效防止了高并發(fā)環(huán)境下可能發(fā)生的緩存擊穿問(wèn)題。
代碼運(yùn)行結(jié)果如下所示:
fetch data from cache
redis: key not found
fetch data from database
v: 程序員陳明勇, shared: true
v: 程序員陳明勇, shared: true
v: 程序員陳明勇, shared: true
v: 程序員陳明勇, shared: true
v: 程序員陳明勇, shared: true
根據(jù)運(yùn)行結(jié)果可知,當(dāng) 5 個(gè) goroutine
并發(fā)獲取相同數(shù)據(jù)時(shí),數(shù)據(jù)獲取操作實(shí)際上只由一個(gè)goroutine
執(zhí)行了一次。此外,由于所有返回的 shared
值均為 true
,這表明返回的結(jié)果被其他 4 個(gè)goroutine
共享。
最佳實(shí)踐
key 的設(shè)計(jì)
在生成 key
的時(shí)候,我們應(yīng)該保證它的唯一性與一致性。
- 唯一性:確保傳遞給
Do
方法的key
具有唯一性,以便Group
區(qū)分不同請(qǐng)求。推薦使用結(jié)構(gòu)化的命名方式來(lái)保證key
的唯一性,例如,可以遵循類似{類型}):{標(biāo)識(shí)}
的規(guī)范來(lái)構(gòu)建key
。以獲取用戶信息為例,相應(yīng)的key
可以是user:1234
,其中user
標(biāo)識(shí)數(shù)據(jù)類型,而1234
則是具體的用戶標(biāo)識(shí)。 - 一致性:對(duì)于相同的請(qǐng)求,無(wú)論何時(shí)調(diào)用,生成的
key
應(yīng)該保持一致,以便Group
正確地合并相同的請(qǐng)求,防止非預(yù)期的錯(cuò)誤。
超時(shí)控制
在調(diào)用 Group.Do
方法時(shí),第一個(gè)到達(dá)的 goroutine
可以成功執(zhí)行 fn
函數(shù),而其他隨后到達(dá)的 goroutine
將進(jìn)入阻塞狀態(tài)。如果阻塞狀態(tài)持續(xù)過(guò)長(zhǎng),可能需要采取降級(jí)策略以保證系統(tǒng)的響應(yīng)性,這時(shí)候,我們可以利用 Group.DoChan
方法和結(jié)合 select
語(yǔ)句實(shí)現(xiàn)超時(shí)控制。
以下是一個(gè)實(shí)現(xiàn)超時(shí)控制的簡(jiǎn)單示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/singleflight/timeout_control/main.go package main import ( "fmt" "golang.org/x/sync/singleflight" "time" ) func main() { var sg singleflight.Group doChan := sg.DoChan("key", func() (interface{}, error) { time.Sleep(4 * time.Second) return"程序員陳明勇", nil }) select { case <-doChan: fmt.Println("done") case <-time.After(2 * time.Second): fmt.Println("timeout") // 采用其他降級(jí)策略 } }
小結(jié)
本文首先介紹了 緩存擊穿 的含義及其常見(jiàn)的解決方案。
然后深入探討了 singleflight
包,從基礎(chǔ)概念、組成部分到具體的安裝和使用示例。
接著通過(guò)模擬一個(gè)典型的并發(fā)訪問(wèn)場(chǎng)景來(lái)演示如何利用 singleflight
來(lái)防止在高并發(fā)場(chǎng)景下可能發(fā)生的緩存擊穿問(wèn)題。
最后,探討在實(shí)踐中設(shè)計(jì) key
和控制請(qǐng)求超時(shí)的最佳策略,以便更好地理解和應(yīng)用 singleflight
,從而優(yōu)化并發(fā)處理邏輯。
到此這篇關(guān)于Go語(yǔ)言使用singleflight解決緩存擊穿的文章就介紹到這了,更多相關(guān)Go singleflight緩存擊穿內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言學(xué)習(xí)筆記之文件讀寫(xiě)操作詳解
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言對(duì)文件進(jìn)行讀寫(xiě)操作的方法,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以參考一下2022-05-05一些關(guān)于Go程序錯(cuò)誤處理的相關(guān)建議
錯(cuò)誤處理在每個(gè)語(yǔ)言中都是一項(xiàng)重要內(nèi)容,眾所周知,通常寫(xiě)程序時(shí)遇到的分為異常與錯(cuò)誤兩種,Golang中也不例外,這篇文章主要給大家介紹了一些關(guān)于Go程序錯(cuò)誤處理的相關(guān)建議,需要的朋友可以參考下2021-09-09解決Golang中g(shù)oroutine執(zhí)行速度的問(wèn)題
這篇文章主要介紹了解決Golang中g(shù)oroutine執(zhí)行速度的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05Go實(shí)現(xiàn)整合Logrus實(shí)現(xiàn)日志打印
這篇文章主要介紹了Go實(shí)現(xiàn)整合Logrus實(shí)現(xiàn)日志打印,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-07-07Go實(shí)現(xiàn)將任何網(wǎng)頁(yè)轉(zhuǎn)化為PDF
在許多應(yīng)用場(chǎng)景中,可能需要將網(wǎng)頁(yè)內(nèi)容轉(zhuǎn)化為?PDF?格式,使用Go編程語(yǔ)言,結(jié)合一些現(xiàn)有的庫(kù),可以非常方便地實(shí)現(xiàn)這一功能,下面我們就來(lái)看看具體實(shí)現(xiàn)方法吧2024-11-11