作者: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物件
func New(defaultExpiration, cleanupInterval time.Duration) *Cache:指定默認有效時間和清除間隔,創建cache物件,- 如果defaultExpiration<1或是NoExpiration,kv中的資料不會被清理,必須手動呼叫介面洗掉,
- 如果cleanupInterval<1,不會自動觸發清理邏輯,要手動觸發c.DeleteExpired(),
func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache:與上面介面的不同是,入參增加了一個map,可以將已有資料按格式構造好,直接創建cache,
-
C(Create):增加一條資料,go-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記錄,- 如果key不存在,增加一個kv記錄;如果key已經存在,用新的value覆寫舊的value,
- 對于有效時間d,如果是0(DefaultExpiration)使用默認有效時間;如果是-1(NoExpiration),表示沒有過期時間,
func (c Cache) SetDefault(k string, x interface{}):與Set用法一樣,只是這里的TTL使用默認有效時間,
-
R(Read):只支持按key進行讀取
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型別的零值,
-
U(Update):按key進行更新
- 直接使用
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相同,
- 直接使用
-
D(Delete)
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包
介紹了go-cache的常用介面,接下來從代碼中看看如何使用,在coding前需要安裝go-cache,命令如下,
go get github.com/patrickmn/go-cache
一個Demo
如何在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回收了,
總結
- go-cache的原始碼代碼里很小,代碼結構和處理邏輯都比較簡單,可以作為golang新手閱讀的很好的素材,
- 對于單機輕量級的記憶體快取如果僅從功能實作角度考慮,go-cache是一個不錯的選擇,使用簡單,
- 但在實際使用中需要注意:
- go-cache沒有對記憶體使用大小或存盤數量進行限制,可能會造成記憶體峰值較高;
- go-cache中存盤的value盡量使用指標型別,相比于存盤物件,不僅在性能上會提高,在記憶體占用上也會有優勢,由于golang的gc機制,map在擴容后原來占用的記憶體不會立刻釋放,因此如果value存盤的是物件會造成占用大量記憶體無法釋放,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/36327.html
標籤:Go
