做者:Moon-Light-Dream
出處:https://www.cnblogs.com/Moon-Light-Dream/
轉載:歡迎轉載,但未經做者贊成,必須保留此段聲明;必須在文章中給出原文鏈接;不然必究法律責任git
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數據加載到內存。github
對於數據庫的基本操做,無外乎關心的CRUD(增刪改查),對應到go-cache中的接口以下:golang
func New(defaultExpiration, cleanupInterval time.Duration) *Cache
:指定默認有效時間和清除間隔,建立cache對象。
func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache
:與上面接口的不一樣是,入參增長了一個map,能夠將已有數據按格式構造好,直接建立cache。func (c Cache) Add(k string, x interface{}, d time.Duration) error
:只有當key不存在或key對應的value已通過期時,能夠增長成功;不然,會返回error。func (c Cache) Set(k string, x interface{}, d time.Duration)
:在cache中增長一條kv記錄。
func (c Cache) SetDefault(k string, x interface{})
:與Set用法同樣,只是這裏的TTL使用默認有效時間。func (c Cache) Get(k string) (interface{}, bool)
:經過key獲取value,若是cache中沒有key,返回的value爲nil,同時返回一個bool類型的參數表示key是否存在。func (c Cache) GetWithExpiration(k string) (interface{}, time.Time, bool)
:與Get接口的區別是,返回參數中增長了key有效期的信息,若是是不會過時的key,返回的是time.Time類型的零值。Set
接口,上面提到若是key已經存在會用新的value覆蓋舊的value,也能夠達到更新的效果。func (c Cache) Replace(k string, x interface{}, d time.Duration) error
:若是key存在且爲過時,將對應value更新爲新的值;不然返回error。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。DecrementXXX
:對於Decrement接口中提到的各類類型,還有對應的接口來處理,同時這些接口能夠獲得value變化後的結果。如func (c *cache) DecrementInt8(k string, n int8) (int8, error)
,從返回值中能夠獲取到value-n後的結果。func (c Cache) Increment(k string, n int64) error
:使用方法與Decrement
相同,將key對應的value加n。IncrementXXX
:使用方法與DecrementXXX
相同。func (c Cache) Delete(k string)
:按照key刪除記錄,若是key不存在直接忽略,不會報錯。func (c Cache) DeleteExpired()
:在cache中刪除全部已通過期的記錄。cache在聲明的時候會指定自動清理的時間間隔,使用者也能夠經過這個接口手動觸發。func (c Cache) Flush()
:將cache清空,刪除全部記錄。func (c Cache) ItemCount() int
:返回cache中的記錄數量。須要注意的是,返回的數值可能會比實際能獲取到的數值大,對於已通過期但尚未即便清理的記錄也會被統計。func (c *cache) OnEvicted(f func(string, interface{}))
:設置一個回調函數(可選項),當一條記錄從cache中刪除(使用者主動delete或cache自助清理過時記錄)時,調用該函數。設置爲nil關閉操做。介紹了go-cache的經常使用接口,接下來從代碼中看看如何使用。在coding前須要安裝go-cache,命令以下。redis
go get github.com/patrickmn/go-cache
如何在golang中使用上述接口實現kv數據庫的增刪改查,接下來看一個demo。其餘更多接口的用法和更詳細的說明,能夠參考GoDoc。數據庫
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對象時傳入的默認有效時間。
const ( NoExpiration time.Duration = -1 DefaultExpiration time.Duration = 0 )
2. Item:cache中存儲的value類型,Object是真正的值,Expiration表示過時時間。可使用Item的```Expired()```接口肯定是否到期,實現方式是過比較當前時間和Item設置的到期時間來判斷是否過時。
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的核心數據結構,其中定義了每條記錄的默認過時時間,底層的存儲結構等信息。
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的時候不會被回收,所以致使了內存泄漏。安全
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回收了。網絡