1. 程式人生 > >Golang package輕量級KV資料快取——go-cache原始碼分析

Golang package輕量級KV資料快取——go-cache原始碼分析

作者:Moon-Light-Dream 出處:https://www.cnblogs.com/Moon-Light-Dream/ 轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任 ## 什麼是go-cache KV儲存引擎有很多,常用的如redis,rocksdb等,如果在實際使用中只是在記憶體中實現一個簡單的kv快取,使用上述引擎就太大費周章了。在Golang中可以使用go-cache這個package實現一個輕量級基於記憶體的kv儲存或快取。GitHub原始碼地址是:https://github.com/patrickmn/go-cache 。 go-cache這個包實際上是在記憶體中實現了一個執行緒安全的map[string]interface{},可以將任何型別的物件作為value,不需要通過網路序列化或傳輸資料,適用於單機應用。對於每組KV資料可以設定不同的TTL(也可以永久儲存),並可以自動實現過期清理。 在使用時一般都是將go-cache作為資料快取來使用,而不是永續性的資料儲存。對於停機後快速恢復的場景,go-cache支援將快取資料儲存到檔案,恢復時從檔案中load資料載入到記憶體。 ## 如何使用go-cache ### 常用介面分析 對於資料庫的基本操作,無外乎關心的CRUD(增刪改查),對應到go-cache中的介面如下: * 建立物件:在使用前需要先建立cache物件 1. ```func New(defaultExpiration, cleanupInterval time.Duration) *Cache```:指定預設有效時間和清除間隔,建立cache物件。 * 如果defaultExpiration<1或是NoExpiration,kv中的資料不會被清理,必須手動呼叫介面刪除。 * 如果cleanupInterval<1,不會自動觸發清理邏輯,要手動觸發c.DeleteExpired()。 2. ```func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache```:與上面介面的不同是,入參增加了一個map,可以將已有資料按格式構造好,直接建立cache。 * C(Create):增加一條資料,go-cache中有幾個介面都能實現新增的功能,但使用場景不同 1. ```func (c Cache) Add(k string, x interface{}, d time.Duration) error```:只有當key不存在或key對應的value已經過期時,可以增加成功;否則,會返回error。 2. ```func (c Cache) Set(k string, x interface{}, d time.Duration)```:在cache中增加一條kv記錄。 * 如果key不存在,增加一個kv記錄;如果key已經存在,用新的value覆蓋舊的value。 * 對於有效時間d,如果是0(DefaultExpiration)使用預設有效時間;如果是-1(NoExpiration),表示沒有過期時間。 3. ```func (c Cache) SetDefault(k string, x interface{})```:與Set用法一樣,只是這裡的TTL使用預設有效時間。 * R(Read):只支援按key進行讀取 1. ```func (c Cache) Get(k string) (interface{}, bool)``` :通過key獲取value,如果cache中沒有key,返回的value為nil,同時返回一個bool型別的引數表示key是否存在。 2. ```func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool)```:與Get介面的區別是,返回引數中增加了key有效期的資訊,如果是不會過期的key,返回的是time.Time型別的零值。 * U(Update):按key進行更新 1. 直接使用```Set```介面,上面提到如果key已經存在會用新的value覆蓋舊的value,也可以達到更新的效果。 2. ```func (c Cache) Replace(k string, x interface{}, d time.Duration) error```:如果key存在且為過期,將對應value更新為新的值;否則返回error。 2. ```func (c Cache) Decrement(k string, n int64) error```:對於cache中value是int, int8, int16, int32, int64, uintptr, uint,uint8, uint32, or uint64, float32,float64這些型別記錄,可以使用該介面,將value值減n。如果key不存在或value不是上述型別,會返回error。 3. ```DecrementXXX```:對於Decrement介面中提到的各種型別,還有對應的介面來處理,同時這些介面可以得到value變化後的結果。如```func (c *cache) DecrementInt8(k string, n int8) (int8, error)```,從返回值中可以獲取到value-n後的結果。 4. ```func (c Cache) Increment(k string, n int64) error```:使用方法與```Decrement```相同,將key對應的value加n。 5. ```IncrementXXX```:使用方法與```DecrementXXX```相同。 * D(Delete) 1. ```func (c Cache) Delete(k string)```:按照key刪除記錄,如果key不存在直接忽略,不會報錯。 2. ```func (c Cache) DeleteExpired()```:在cache中刪除所有已經過期的記錄。cache在宣告的時候會指定自動清理的時間間隔,使用者也可以通過這個介面手動觸發。 3. ```func (c Cache) Flush()```:將cache清空,刪除所有記錄。 * 其他介面: 1. ```func (c Cache) ItemCount() int```:返回cache中的記錄數量。**需要注意的是,返回的數值可能會比實際能獲取到的數值大,對於已經過期但還沒有即使清理的記錄也會被統計。** 2. ```func (c *cache) OnEvicted(f func(string, interface{}))```:設定一個回撥函式(可選項),當一條記錄從cache中刪除(使用者主動delete或cache自助清理過期記錄)時,呼叫該函式。設定為nil關閉操作。 ### 安裝go-cache包 介紹了go-cache的常用介面,接下來從程式碼中看看如何使用。在coding前需要安裝go-cache,命令如下。 ``` go get github.com/patrickmn/go-cache ``` ### 一個Demo 如何在golang中使用上述介面實現kv資料庫的增刪改查,接下來看一個demo。其他更多介面的用法和更詳細的說明,可以參考[GoDoc](https://godoc.org/github.com/patrickmn/go-cache)。 ```golang import ( "fmt" "time" "github.com/patrickmn/go-cache" // 使用前先import包 ) func main() { // 建立一個cache物件,預設ttl 5分鐘,每10分鐘對過期資料進行一次清理 c := cache.New(5*time.Minute, 10*time.Minute) // Set一個KV,key是"foo",value是"bar" // TTL是預設值(上面建立物件的入參,也可以設定不同的值)5分鐘 c.Set("foo", "bar", cache.DefaultExpiration) // Set了一個沒有TTL的KV,只有呼叫delete介面指定key時才會刪除 c.Set("baz", 42, cache.NoExpiration) // 從cache中獲取key對應的value foo, found := c.Get("foo") if found { fmt.Println(foo) } // 如果想提高效能,儲存指標型別的值 c.Set("foo", &MyStruct, cache.DefaultExpiration) if x, found := c.Get("foo"); found { foo := x.(*MyStruct) // ... } } ``` ## 原始碼分析 1. 常量:內部定義的兩個常量`NoExpiration`和`DefaultExpiration`,可以作為上面介面中的入參,`NoExpiration`表示沒有設定有效時間,`DefaultExpiration`表示使用New()或NewFrom()建立cache物件時傳入的預設有效時間。 ```golang const ( NoExpiration time.Duration = -1 DefaultExpiration time.Duration = 0 ) ``` 2. Item:cache中儲存的value型別,Object是真正的值,Expiration表示過期時間。可以使用Item的```Expired()```介面確定是否到期,實現方式是過比較當前時間和Item設定的到期時間來判斷是否過期。 ```golang type Item struct { Object interface{} Expiration int64 } func (item Item) Expired() bool { if item.Expiration == 0 { return false } return time.Now().UnixNano() > item.Expiration } ``` 3. cache:go-cache的核心資料結構,其中定義了每條記錄的預設過期時間,底層的儲存結構等資訊。 ```golang type cache struct { defaultExpiration time.Duration // 預設過期時間 items map[string]Item // 底層儲存結構,使用map實現 mu sync.RWMutex // map本身非執行緒安全,操作時需要加鎖 onEvicted func(string, interface{}) // 回撥函式,當記錄被刪除時觸發相應操作 janitor *janitor // 用於定時輪詢失效的key } ``` 4. janitor:用於定時輪詢失效的key,其中定義了輪詢的週期和一個無快取的channel,用來接收結束資訊。 ``` type janitor struct { Interval time.Duration // 定時輪詢週期 stop chan bool // 用來接收結束資訊 } func (j *janitor) Run(c *cache) { ticker := time.NewTicker(j.Interval) // 建立一個timeTicker定時觸發 for { select { case <-ticker.C: c.DeleteExpired() // 呼叫DeleteExpired介面處理刪除過期記錄 case <-j.stop: ticker.Stop() return } } } ``` **對於janitor的處理,這裡使用的技巧值得學習 **,下面這段程式碼是在New() cache物件時,會同時開啟一個goroutine跑janitor,在run之後可以看到做了`runtime.SetFinalizer`的處理,這樣處理了可能存在的記憶體洩漏問題。 ``` func stopJanitor(c *Cache) { c.janitor.stop <- true } func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { c := newCache(de, m) // This trick ensures that the janitor goroutine (which--granted it // was enabled--is running DeleteExpired on c forever) does not keep // the returned C object from being garbage collected. When it is // garbage collected, the finalizer stops the janitor goroutine, after // which c can be collected. C := &Cache{c} if ci > 0 { runJanitor(c, ci) runtime.SetFinalizer(C, stopJanitor) } return C } ``` 可能的洩漏場景如下,使用者建立了一個cache物件,在使用後置為nil,在使用者看來在gc的時候會被回收,但是因為有goroutine在引用,在gc的時候不會被回收,因此導致了記憶體洩漏。 ```golang c := cache.New() // do some operation c = nil ``` 解決方案可以增加Close介面,在使用後呼叫Close介面,通過channel傳遞資訊結束goroutine,但如果使用者在使用後忘了呼叫Close介面,還是會造成記憶體洩漏。 另外一種解決方法是使用`runtime.SetFinalizer`,不需要使用者顯式關閉, gc在檢查C這個物件沒有引用之後, gc會執行關聯的SetFinalizer函式,主動終止goroutine,並取消物件C與SetFinalizer函式的關聯關係。這樣下次gc時,物件C沒有任何引用,就可以被gc回收了。 ## 總結 1. go-cache的原始碼程式碼裡很小,程式碼結構和處理邏輯都比較簡單,可以作為golang新手閱讀的很好的素材。 2. 對於單機輕量級的記憶體快取如果僅從功能實現角度考慮,go-cache是一個不錯的選擇,使用簡單。 3. 但在實際使用中需要注意: * go-cache沒有對記憶體使用大小或儲存數量進行限制,可能會造成記憶體峰值較高; * go-cache中儲存的value儘量使用指標型別,相比於儲存物件,不僅在效能上會提高,在記憶體佔用上也會有優勢。由於golang的gc機制,map在擴容後原來佔用的記憶體不會立刻釋放,因此如果value儲存的是物件會造成佔用大量記憶體無法