大家好,我是豆小匠。
這期來閱讀go-cache的源碼,了解本地緩存的實現方式,同時掌握一些閱讀源碼的技巧~
1. 源碼獲取
git clone https://github.com/patrickmn/go-cache.git
用Goland打開可以看到真正實現功能的也就兩個go文件,cache.go 1162行,sharded.go 193行,共1355行,用來作為源碼閱讀的練手素材是非常合適的。
通過README.md文件,可以了解這個包的使用方法:
import ("fmt""github.com/patrickmn/go-cache""time"
)func main() {// 創建一個緩存對象,默認過期時間5分鐘,每10分鐘清理一次緩存c := cache.New(5*time.Minute, 10*time.Minute)// 設置緩存key:foo,value:bar,過期時間是包里定義的一個常量,一會看看具體定義了啥c.Set("foo", "bar", cache.DefaultExpiration)// 獲取key為foo的緩存,通過類型斷言獲取原始的數據foo, found := c.Get("foo")if found {MyFunction(foo.(string))}
}
2. 源碼閱讀
上面我們看到,創建一個緩存實例,需要傳入緩存清理的間隔,也就是說緩存的刪除不是根據緩存過期時間實時刪除的,那怎么處理才能讓已過期的緩存在邏輯上失效呢?
帶著疑問,開始閱讀cache.go文件。
2.1. Cache定義
type Cache struct {*cache // 為何套娃,先按下不表
}type cache struct {defaultExpiration time.Duration // 默認過期時間items map[string]Item // 所有緩存key value,用一個map保存,key是string,value是一個結構體Itemmu sync.RWMutex // 讀寫鎖,可以知道go-cache大概率是并發安全的onEvicted func(string, interface{}) // 這啥,先不管janitor *janitor // 這啥,先不管
}type Item struct {Object interface{} // 真正存儲的緩存數據Expiration int64 // 這個數據的過期時間
}
看完Cache結構體的定義,先有個整體印象,再看它的方法實現~
2.2. Cache初始化
在README.go,我們已經知道,初始化的函數是New(defaultExpiration, cleanupInterval time.Duration),雙擊shift,輸入New,就能找到這個函數。
type janitor struct {Interval time.Duration // 清理過期緩存的間隔stop chan bool // 接受停止協程的信號
}func New(defaultExpiration, cleanupInterval time.Duration) *Cache {items := make(map[string]Item) // 定義緩存容器,會存到cache對象的itemsreturn newCacheWithJanitor(defaultExpiration, cleanupInterval, items) // 創建一個帶有清理協程的Cache對象
}func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {c := newCache(de, m) // 生成小寫那個cache對象(私有)C := &Cache{c}if ci > 0 { // 傳入定時刪除緩存時間大于0,啟動看清理協程runJanitor(c, ci) // 啟動清理協程,定時刪除過期的cache keyruntime.SetFinalizer(C, stopJanitor) // 設置C被回收時,執行函數停止清理協程}return C
}
runtime.SetFinalizer:對象可以關聯一個SetFinalizer函數, 當gc檢測到unreachable對象有關聯的SetFinalizer函數時,會執行關聯的SetFinalizer函數, 同時取消關聯。 這樣當下一次gc的時候,對象重新處于unreachable狀態并且沒有SetFinalizer關聯, 就會被回收。
通過上面源碼的閱讀,我們可以知道:
- 清理過期緩存通過一個清理協程定期清理。
- 當Cache不可達時,GC會觸發停止janitor協程的函數,下一次GC,Cache和cache(內部cache對象)都會被回收。(如果janitor協程和Cache綁定,Cache對象不會被回收,有內存泄露的風險)
c := cache.New(5*time.Minute, 10*time.Minute)
c = nil // 這里cache已經不使用了,第一次GC會執行SetFinalizer函數,停掉清理協程,第二次GC則會把Cache和cache對象都回收掉
如果清理協程綁定在Cache對象,因為協程一直在運行,即使在使用者看來c已經設置為nil,cache不再使用,GC也無法回收Cache。
2.3. 緩存失效判斷
Cache上是不掛方法的,方法都掛在內部對象cache上。
我們先看Get方法:
func (c *cache) Get(k string) (interface{}, bool) {c.mu.RLock() // 加讀鎖item, found := c.items[k]if !found {c.mu.RUnlock()return nil, false}// 下面這里會判斷item里的過期時間,過期時間小于當前時間,則在邏輯上失效,返回nil, falseif item.Expiration > 0 { // 如果expiration為0,說明設置的是永不過期if time.Now().UnixNano() > item.Expiration {c.mu.RUnlock()return nil, false}}c.mu.RUnlock()return item.Object, true
}
看源碼可以很清晰的看到,緩存過期不是通過是否存在key來判斷的,而是通過item里存的expiration時間來判斷,因此定時清理緩存是為了清理空間。
2.4. 總體梳理
其他方法都非常明確,我們可以挑幾個常用的看看實現,最后整理下cache這個類的成員變量和方法,畫個圖,完事!
前面埋的坑:onEvicted 是刪除key的回調函數。
另外sharded.go文件是一個實驗性的代碼,用于緩存分片,目前還沒對外暴露。