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