Go中使用sync.Map實現線程安全的緩存
緩存是優化現代應用程序性能的關鍵方面。它允許您存儲并快速檢索昂貴操作的結果或經常訪問的數據,減少了反復重新計算或獲取數據的需要。在本文中,我們將探討如何使用sync.Map包在Go中實現線程安全的緩存。這種緩存實現支持緩存條目的過期,確保過時的數據不會滯留在緩存中。

為什么要費心
在我們開始實現自己的線程安全內存緩存之前,讓我們考慮一下其優缺點。考慮到替代方案是使用為緩存而發明的、有長期使用和支持歷史的外部庫(工具),讓我們思考一下優點和缺點。
使用Go的sync.Map實現自己的線程安全緩存相比使用像Redis這樣的外部庫有幾個優點,這取決于您的用例和要求。以下是使用sync.Map創建自己的緩存可能有優勢的一些原因:
- 更低的延遲:使用像sync.Map這樣的內存緩存時,數據存儲在應用程序的內存中。這可能導致比需要應用程序和緩存服務之間的網絡通信的單獨服務,如Redis,有更低的訪問延遲。
- 更簡單的部署:使用基于sync.Map的緩存,無需部署、配置和維護像Redis這樣的額外服務。您的緩存解決方案是應用程序的一部分,使部署過程更簡單,并可能減少操作復雜性。
- 減少資源使用:與像Redis這樣的外部服務相比,使用sync.Map的內存緩存通常消耗更少的資源,從而節省了內存和CPU使用。這對于小規模的應用程序或資源緊張的應用程序可能更加經濟高效。
- 更容易集成:在Go應用程序中直接使用sync.Map實現緩存可以更容易地與現有的代碼庫集成。您不需要學習新的API或管理到外部服務的連接。
- 定制性:創建自己的緩存實現時,您可以完全控制其行為和功能。您可以輕松地根據具體需求調整緩存,針對您的用例進行優化,并根據需要添加自定義過期策略或其他功能。
- 樂趣:創建實現緩存的自己的代碼段會帶來很多樂趣,并幫助更好地理解提供緩存功能的外部庫。更好地理解它們有助于更好地利用它們提供的所有功能。
但是,值得注意的是,使用像Redis這樣的外部緩存解決方案對于較大規模的應用程序或那些有更復雜的緩存需求的應用程序有其自身的一系列優勢。使用Redis的一些好處包括:
- 可擴展性:Redis設計用于高性能,并可以水平擴展以處理大量請求和數據大小。
- 持久性:Redis支持不同級別的數據持久性,確保您的緩存數據在重啟或崩潰后仍然存在。
- 高級功能:除了簡單的鍵值緩存外,Redis還提供了一系列功能,如數據結構、發布/訂閱消息等。
最終,選擇使用sync.Map實現自己的緩存還是使用像Redis這樣的外部庫將取決于您的具體需求、應用程序的規模以及您在性能、復雜性和資源方面愿意做的權衡。
此外,實現您的緩存會帶來樂趣并幫助更好地理解像Redis這樣的更復雜的產品。因此,我們將在此文章中實現一個。
為什么我們使用sync.Map
簡單地說,因為它完美地滿足了我們的需要。更深入的解釋 - sync.Map是Go標準庫中的一個并發的、線程安全的map實現。它設計用于在多個goroutine并發訪問映射的情況下使用,并且鍵的數量是未知的或隨時間變化的。
值得注意的是,雖然sync.Map是特定用例的一個很好的選擇,但它并不意味著要替換所有場景的內置map類型。特別是,sync.Map最適合以下情況:
- 映射主要是讀取密集型,偶爾寫入。
- 鍵的數量隨時間變化或事先不知道。
- 映射由多個goroutine并發訪問。
在鍵的數量是固定的或事先知道的情況下,且映射可以預先分配,使用適當的同步如sync.Mutex或sync.RWMutex的內置map類型可能會提供更好的性能。
創建SafeCache
如上所述,我們的SafeCache是一個簡單的、線程安全的緩存,使用Go的sync.Map存儲其鍵值對。
首先,我們定義一個CacheEntry結構來保存值及其過期時間戳:
type CacheEntry struct {
value interface{}
expiration int64
}在SafeCache結構中嵌入了一個sync.Map,它提供了對鍵值對的并發安全訪問:
type SafeCache struct {
syncMap sync.Map
}向緩存中添加值
然后我們定義了一個 Set 方法,該方法允許我們在緩存中存儲一個帶有指定生存時間(TTL,Time To Live)的值。TTL 決定了緩存條目應被認為有效的時間長度。一旦 TTL 過期,在下一個清理周期中將會移除該緩存條目。
func (sc *SafeCache) Set(key string, value interface{}, ttl time.Duration) {
expiration := time.Now().Add(ttl).UnixNano()
sc.syncMap.Store(key, CacheEntry{value: value, expiration: expiration})
}從緩存中檢索值
接下來需要的方法是 Get,它使用鍵從緩存中檢索值。如果沒有找到該值或該值已過期,該方法將返回 false:
func (sc *SafeCache) Get(key string) (interface{}, bool) {
// ... (see the provided code for the full implementation)
}在 Get 方法中重要的是從緩存加載值后進行類型斷言。我們依賴于 sync.Map 的 Load 方法,該方法返回接口。
entry, found := sc.syncMap.Load(key)
if !found {
return nil, false
}
// Type assertion to CacheEntry, as entry is an interface{}
cacheEntry := entry.(CacheEntry)從緩存中移除值
當然,我們還需要一個 Delete 方法,使我們能夠從緩存中移除一個值:
func (sc *SafeCache) Delete(key string) {
sc.syncMap.Delete(key)
}清理過期條目
我們通過 CleanUp 方法擴展了緩存,該方法負責定期從緩存中刪除過期的條目。它使用 sync.Map 提供的 Range 方法遍歷緩存中的所有鍵值對,并刪除那些TTL已過期的條目:
func (sc *SafeCache) CleanUp() {
// ... (see the provided code for the full implementation)
}要運行 CleanUp 方法,我們可以在初始化緩存時啟動一個單獨的 Goroutine:
cache := &SafeCache{}
go cache.CleanUp()完整的代碼片段
package cache
import (
"sync"
"time"
)
// CacheEntry is a value stored in the cache.
type CacheEntry struct {
value interface{}
expiration int64
}
// SafeCache is a thread-safe cache.
type SafeCache struct {
syncMap sync.Map
}
// Set stores a value in the cache with a given TTL
// (time to live) in seconds.
func (sc *SafeCache) Set(key string, value interface{}, ttl time.Duration) {
expiration := time.Now().Add(ttl).UnixNano()
sc.syncMap.Store(key, CacheEntry{value: value, expiration: expiration})
}
// Get retrieves a value from the cache. If the value is not found
// or has expired, it returns false.
func (sc *SafeCache) Get(key string) (interface{}, bool) {
entry, found := sc.syncMap.Load(key)
if !found {
return nil, false
}
// Type assertion to CacheEntry, as entry is an interface{}
cacheEntry := entry.(CacheEntry)
if time.Now().UnixNano() > cacheEntry.expiration {
sc.syncMap.Delete(key)
return nil, false
}
return cacheEntry.value, true
}
// Delete removes a value from the cache.
func (sc *SafeCache) Delete(key string) {
sc.syncMap.Delete(key)
}
// CleanUp periodically removes expired entries from the cache.
func (sc *SafeCache) CleanUp() {
for {
time.Sleep(1 * time.Minute)
sc.syncMap.Range(func(key, entry interface{}) bool {
cacheEntry := entry.(CacheEntry)
if time.Now().UnixNano() > cacheEntry.expiration {
sc.syncMap.Delete(key)
}
return true
})
}
}最后,你可以運行以下的 main.go 程序來檢查緩存是否工作。我們創建了一個HTTP服務器,它在“/compute”端點監聽請求。該服務器接受一個整數n作為查詢參數,并返回昂貴計算的結果(在這種情況下,帶有模擬延遲的簡單平方操作)。服務器首先檢查緩存,看看給定輸入的結果是否已經被緩存;如果沒有,它會計算結果,將其存儲在緩存中,并將其返回給客戶端。
要測試服務器,運行代碼并請求http://localhost:8080/compute?n=5。第一個請求會花費更長的時間(由于模擬的延遲),但具有相同n的后續請求將立即返回緩存的結果。
package main
import (
"fmt"
"log"
"net/http"
"safe-cache/cache"
"strconv"
"time"
)
func expensiveComputation(n int) int {
// Simulate an expensive computation
time.Sleep(2 * time.Second)
return n * n
}
func main() {
safeCache := &cache.SafeCache{}
// Start a goroutine to periodically clean up the cache
go safeCache.CleanUp()
http.HandleFunc("/compute", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
n, err := strconv.Atoi(query.Get("n"))
if err != nil {
http.Error(w, "Invalid input", http.StatusBadRequest)
return
}
cacheKey := fmt.Sprintf("result_%d", n)
cachedResult, found := safeCache.Get(cacheKey)
var result int
if found {
result = cachedResult.(int)
} else {
result = expensiveComputation(n)
safeCache.Set(cacheKey, result, 1*time.Minute)
}
_, err = fmt.Fprintf(w, "Result: %d\n", result)
if err != nil {
return
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}結論
在本文中,我們展示了如何使用sync.Map包在Go中實現一個簡單、線程安全的緩存。
這個緩存實現支持基于TTL的過期的鍵值存儲,并可以輕松地集成到你的Go應用中,以提高性能并減少對你的數據源或計算資源的負載。
































