初衷:
之前是java工程師,最近在轉go,簡單學習了go的相關語言知識,想通過看些簡單的源代碼來提升下。go-cache是一套go語言實作的單機本地緩存的package,可以友善的建構記憶體緩存,代碼也比較簡單。
基本的介紹:
下面這些都有一些詳細的使用示例,可以去參考使用:
github代碼位址:https://github.com/patrickmn/go-cache
godoc: https://godoc.org/github.com/patrickmn/go-cache
代碼分析:
建構緩存需要考慮幾個基本點:
- 存儲格式
- 替換政策
-
失效政策
還有一些其他的考慮點,可以參考之前的文章,但是基本點就這些。
一: 存儲
在gocache實作中,底層存儲的key-value對類型做了基本限制,key要求是string,value内部封裝了Item對象,結構如下,僅增加了當個value的實效時間
type Item struct {
Object interface{}
Expiration int64
}
核心的存儲格式
type cache struct {
defaultExpiration time.Duration //預設的通用key實效時長
items map[string]Item //底層的map存儲
mu sync.RWMutex //由于map是非線程安全的,增加的全局鎖
onEvicted func(string, interface{})//失效key時,回觸發,我自己命名為回收函數
janitor *janitor //螢幕,Goroutine,定時輪詢用于失效key
}
以上是cache的結構,set、get基本都是對items進行操作,寫的時候用mu加鎖,保證線程安全。
這塊有個很巧妙的設計,感覺很贊,解決互相引用的問題
首先,janitor是用于cleanup的政策對象,基本結構如下:
type janitor struct {
Interval time.Duration //定時器
stop chan bool //goroutine的控制開關
}
在構造cache的時候,如果有設定主動失效時間間隔,會在cache上綁定janitor線程,定時輪詢items,對于失效的從items中剔除,如下:
//注意,janator會有cache的引用
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval)
for {
select {
case
這個地方有個需要注意的事情,cache中綁定了janitor,而janitor run的流程中也有cache的引用,相當于循環引用了,go的垃圾回收政策是引用計數法,這種情況下,很容易造成記憶體洩漏。
為了解決這個問題,引入了Cache對象(大寫的),内嵌了cache對象,對外暴露的是Cache,對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)
runtime.SetFinalizer(C, stopJanitor) //關鍵
}
return C
}
type Cache struct {
*cache
}
當外面的Cache對象指向發生變化時,Cache的引用數量為0,是以gc可以回收,但是對于cache而言,循環引用的問題依然存在,比較巧妙的是
runtime.SetFinalizer(C, stopJanitor)
在回收Cache時,stop了cleanup線程,斷開了引用,是的cache也可以被正常回收,不會産生記憶體洩漏,感覺這種寫法很好玩。
二:替換政策、失效政策
gocache相對簡單,用了map[string]Item來進行存儲,沒有限制大小,隻要記憶體允許可以一直存,沒有上限,這個在實際生産中需要注意。
其他的說明:
gocache很簡單,但是也有不少問題沒有做,簡單列一些自己想到的,可以一起優化下:
- cache數量沒有上限,這個線上上使用的時候還是容易出問題
- 調用get擷取對象的時候,如果對象不存在,get方法會直接傳回nil,交給上層處理,實際的業務邏輯中,通常都會去redis或者db等持久化資料的地方去查,參考guava cache,感覺可以寫成loader的方式,if-not-exists時,直接回調loader方法
- 鎖的粒度問題,為了保證線程安全,整個cache上鎖,進行操作,會對性能有所影響,這塊後續可以考慮用細粒度的鎖,像concurrentHashMap或者guava cache那樣,實作分段鎖的機制
- 一些cache的命中名額沒辦法跟蹤
總結下:
gocache是一種比較簡單的機制,适用于那些緩存資料量不大的本地緩存建構,而且防止記憶體洩漏的方式值得借鑒