Go 緩存系列之 go-cache
一句話描述
go-cache [1] 基於內存的 K/V 存儲 / 緩存 : (類似於 Memcached),適用於單機應用程序
簡介
go-cache 是什麼?
基於內存的 K/V 存儲 / 緩存 : (類似於 Memcached),適用於單機應用程序 ,支持刪除,過期,默認 Cache 共享鎖,
大量 key 的情況下會造成鎖競爭嚴重
爲什麼選擇 go-cache?
可以存儲任何對象(在給定的持續時間內或永久存儲),並且可以由多個 goroutine 安全地使用緩存。
Example
package main
import (
"fmt"
"time"
"github.com/patrickmn/go-cache"
)
type MyStruct struct {
Name string
}
func main() {
// 設置超時時間和清理時間
c := cache.New(5*time.Minute, 10*time.Minute)
// 設置緩存值並帶上過期時間
c.Set("foo", "bar", cache.DefaultExpiration)
// 設置沒有過期時間的KEY,這個KEY不會被自動清除,想清除使用:c.Delete("baz")
c.Set("baz", 42, cache.NoExpiration)
var foo interface{}
var found bool
// 獲取值
foo, found = c.Get("foo")
if found {
fmt.Println(foo)
}
var foos string
// 獲取值, 並斷言
if x, found := c.Get("foo"); found {
foos = x.(string)
fmt.Println(foos)
}
// 對結構體指針進行操作
var my *MyStruct
c.Set("foo", &MyStruct{Name: "NameName"}, cache.DefaultExpiration)
if x, found := c.Get("foo"); found {
my = x.(*MyStruct)
// ...
}
fmt.Println(my)
}
源碼分析
源碼分析主要針對核心的存儲結構、Set、Get、Delete、定時清理邏輯進行分析。包含整體的邏輯架構
核心的存儲結構
package cache
// Item 每一個具體緩存值
type Item struct {
Object interface{}
Expiration int64 // 過期時間:設置時間+緩存時長
}
// Cache 整體緩存
type Cache struct {
*cache
}
// cache 整體緩存
type cache struct {
defaultExpiration time.Duration // 默認超時時間
items map[string]Item // KV對
mu sync.RWMutex // 讀寫鎖,在操作(增加,刪除)緩存時使用
onEvicted func(string, interface{}) // 刪除KEY時的CallBack函數
janitor *janitor // 定時清空緩存的結構
}
// janitor 定時清空緩存的結構
type janitor struct {
Interval time.Duration // 多長時間掃描一次緩存
stop chan bool // 是否需要停止
}
Set
package cache
func (c *cache) Set(k string, x interface{}, d time.Duration) {
// "Inlining" of set
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
c.mu.Lock() // 這裏可以使用defer?
c.items[k] = Item{
Object: x, // 實際的數據
Expiration: e, // 下次過期時間
}
c.mu.Unlock()
}
Get
package cache
func (c *cache) Get(k string) (interface{}, bool) {
c.mu.RLock() // 加鎖,限制併發讀寫
item, found := c.items[k] // 在 items 這個 map[string]Item 查找數據
if !found {
c.mu.RUnlock()
return nil, false
}
if item.Expiration > 0 {
if time.Now().UnixNano() > item.Expiration { // 已經過期,直接返回nil,爲什麼在這裏不直接就刪除了呢?
c.mu.RUnlock()
return nil, false
}
}
c.mu.RUnlock()
return item.Object, true
}
Delete
package cache
// Delete an item from the cache. Does nothing if the key is not in the cache.
func (c *cache) Delete(k string) {
c.mu.Lock()
v, evicted := c.delete(k)
c.mu.Unlock()
if evicted {
c.onEvicted(k, v) // 刪除KEY時的CallBack
}
}
func (c *cache) delete(k string) (interface{}, bool) {
if c.onEvicted != nil {
if v, found := c.items[k]; found {
delete(c.items, k)
return v.Object, true
}
}
delete(c.items, k)
return nil, false
}
定時清理邏輯
package cache
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
c := newCache(de, m)
C := &Cache{c}
if ci > 0 {
runJanitor(c, ci) // 定時運行清除過期KEY
runtime.SetFinalizer(C, stopJanitor) // 當C被GC回收時,會停止runJanitor 中的協程
}
return C
}
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),
}
c.janitor = j
go j.Run(c) // 新的協程做過期刪除邏輯
}
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval)
for {
select {
case <-ticker.C: // 每到一個週期就全部遍歷一次
c.DeleteExpired() // 實際的刪除邏輯
case <-j.stop:
ticker.Stop()
return
}
}
}
// Delete all expired items from the cache.
func (c *cache) DeleteExpired() {
var evictedItems []keyAndValue
now := time.Now().UnixNano()
c.mu.Lock()
for k, v := range c.items { // 加鎖遍歷整個列表
// "Inlining" of expired
if v.Expiration > 0 && now > v.Expiration {
ov, evicted := c.delete(k)
if evicted {
evictedItems = append(evictedItems, keyAndValue{k, ov})
}
}
}
c.mu.Unlock()
for _, v := range evictedItems {
c.onEvicted(v.key, v.value)
}
}
思考
Lock 的使用
- 在 go-cache 中,涉及到讀寫 cache,基本上都用到了鎖,而且在遍歷的時候也用到鎖,當 cache 的數量非常多時,讀寫頻繁時, 會有嚴重的鎖衝突。
使用讀寫鎖?
- sync.RWMutex, 在讀的時候加 RLock, 可以允許多個讀。在寫的時候加 Lock,不允許其他讀和寫。
鎖的粒度是否可以變更小?
- 根據 KEY HASH 到不同的 map 中
使用 sync.map?
- 減少鎖的使用
runtime.SetFinalizer
在實際的編程中,我們都希望每個對象釋放時執行一個方法,在該方法內執行一些計數、釋放或特定的要求, 以往都是在對象指針置 nil 前調用一個特定的方法, golang 提供了 runtime.SetFinalizer 函數,當 GC 準備釋放對象時,會回調該函數指定的方法,非常方便和有效。
對象可以關聯一個 SetFinalizer 函數, 當 gc 檢測到 unreachable 對象有關聯的 SetFinalizer 函數時, 會執行關聯的 SetFinalizer 函數, 同時取消關聯。這樣當下一次 gc 的時候, 對象重新處於 unreachable 狀態 並且沒有 SetFinalizer 關聯, 就會被回收。
Doc
https://pkg.go.dev/github.com/patrickmn/go-cache
比較
相似的庫
-
https://github.com/golang/groupcache
-
https://github.com/allegro/bigcache
-
https://github.com/coocood/freecache
參考資料
[1] go-cache : https://github.com/patrickmn/go-cache
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/hz7mViPQIwca0mSylfS-ww