gcache是一個用go實現的併發安全的本地緩存庫。他能夠實現以下功能:git
其實github上已經有了很詳細的例子,其中有簡單key/value、設置超時時間、設置淘汰策略、設置回調函數等各類例子。這裏簡單摘抄一些簡單的例子:github
package main import ( "github.com/bluele/gcache" "fmt" ) func main() { gc := gcache.New(20). LRU(). Build() gc.Set("key", "ok") value, err := gc.Get("key") if err != nil { panic(err) } fmt.Println("Get:", value) } Get: ok
package main import ( "github.com/bluele/gcache" "fmt" "time" ) func main() { gc := gcache.New(20). LRU(). Build() gc.SetWithExpire("key", "ok", time.Second*10) value, _ := gc.Get("key") fmt.Println("Get:", value) // Wait for value to expire time.Sleep(time.Second*10) value, err = gc.Get("key") if err != nil { panic(err) } fmt.Println("Get:", value) } Get: ok // 10 seconds later, new attempt: panic: ErrKeyNotFound
package main import ( "github.com/bluele/gcache" "fmt" ) func main() { gc := gcache.New(20). LRU(). LoaderFunc(func(key interface{}) (interface{}, error) { return "ok", nil }). Build() value, err := gc.Get("key") if err != nil { panic(err) } fmt.Println("Get:", value) } Get: ok
// 緩存builder對象,存放時間、大小和各類回調函數 type CacheBuilder struct { clock Clock tp string size int loaderExpireFunc LoaderExpireFunc evictedFunc EvictedFunc purgeVisitorFunc PurgeVisitorFunc addedFunc AddedFunc expiration *time.Duration deserializeFunc DeserializeFunc serializeFunc SerializeFunc }
// 設置策略 設置CacheBuilder的回調函數屬性 func (cb *CacheBuilder) LRU() *CacheBuilder { return cb.EvictType(TYPE_LRU) } // 設置過時時間 設置CacheBuilder的Expiration屬性 func (cb *CacheBuilder) Expiration(expiration time.Duration) *CacheBuilder { cb.expiration = &expiration return cb } // 設置驅除回調函數 func (cb *CacheBuilder) EvictedFunc(evictedFunc EvictedFunc) *CacheBuilder { cb.evictedFunc = evictedFunc return cb }
// 判斷size和類型 func (cb *CacheBuilder) Build() Cache { if cb.size <= 0 && cb.tp != TYPE_SIMPLE { panic("gcache: Cache size <= 0") } return cb.build() } // 根據type來新建相對應的cache對象 func (cb *CacheBuilder) build() Cache { switch cb.tp { case TYPE_SIMPLE: return newSimpleCache(cb) case TYPE_LRU: return newLRUCache(cb) case TYPE_LFU: return newLFUCache(cb) case TYPE_ARC: return newARC(cb) default: panic("gcache: Unknown type " + cb.tp) } } // 舉例一個SimpleCache func newSimpleCache(cb *CacheBuilder) *SimpleCache { c := &SimpleCache{} buildCache(&c.baseCache, cb) c.init() c.loadGroup.cache = c return c } // init 初始化simple 中的map func (c *SimpleCache) init() { if c.size <= 0 { c.items = make(map[interface{}]*simpleItem) } else { c.items = make(map[interface{}]*simpleItem, c.size) } } // 初始化回調函數 func buildCache(c *baseCache, cb *CacheBuilder) { c.clock = cb.clock c.size = cb.size c.loaderExpireFunc = cb.loaderExpireFunc c.expiration = cb.expiration c.addedFunc = cb.addedFunc c.deserializeFunc = cb.deserializeFunc c.serializeFunc = cb.serializeFunc c.evictedFunc = cb.evictedFunc c.purgeVisitorFunc = cb.purgeVisitorFunc c.stats = &stats{} }
type Cache interface { Set(key, value interface{}) error SetWithExpire(key, value interface{}, expiration time.Duration) error Get(key interface{}) (interface{}, error) GetIFPresent(key interface{}) (interface{}, error) GetALL(checkExpired bool) map[interface{}]interface{} get(key interface{}, onLoad bool) (interface{}, error) Remove(key interface{}) bool Purge() Keys(checkExpired bool) []interface{} Len(checkExpired bool) int Has(key interface{}) bool statsAccessor } type statsAccessor interface { HitCount() uint64 MissCount() uint64 LookupCount() uint64 HitRate() float64 } type baseCache struct { clock Clock size int loaderExpireFunc LoaderExpireFunc evictedFunc EvictedFunc purgeVisitorFunc PurgeVisitorFunc addedFunc AddedFunc deserializeFunc DeserializeFunc serializeFunc SerializeFunc expiration *time.Duration mu sync.RWMutex loadGroup Group *stats }
SimpleCache是gcache中最簡單的一種,其中比較重要的函數就是Get,Set。
在SimpleCache結構體中items保存這simpleItem。simpleItem結構體中保存具體值和過時時間。
Get,Set函數就是經過操做items屬性來保存和獲取緩存中的值的。下面咱們詳細看一下代碼:算法
type SimpleCache struct { baseCache items map[interface{}]*simpleItem } type simpleItem struct { clock Clock value interface{} expiration *time.Time }
func (c *SimpleCache) set(key, value interface{}) (interface{}, error) { var err error // 判斷是否有序列化函數 有則執行回調函數 if c.serializeFunc != nil { value, err = c.serializeFunc(key, value) if err != nil { return nil, err } } // 檢查是否存在key item, ok := c.items[key] if ok { item.value = value } else { // 檢查是否超過設置的大小範圍 if (len(c.items) >= c.size) && c.size > 0 { // 若是超過大小則驅逐一個 c.evict(1) } // 組成simpleItem對象 item = &simpleItem{ clock: c.clock, value: value, } c.items[key] = item } // 判斷是否有過時時間 if c.expiration != nil { // 若是有則設置過時時間 t := c.clock.Now().Add(*c.expiration) item.expiration = &t } // 判斷是否有添加函數 有則添加 if c.addedFunc != nil { c.addedFunc(key, value) } return item, nil } // SimpleCache 驅逐方法 // 驅逐策略則是最簡單的淘汰一個,由於map的特性 range訪問的是隨機的數據。因此驅逐出去的數據也是隨機的一個。 func (c *SimpleCache) evict(count int) { now := c.clock.Now() current := 0 for key, item := range c.items { if current >= count { return } if item.expiration == nil || now.After(*item.expiration) { defer c.remove(key) current++ } } }
// get函數 從緩存中獲取數據 func (c *SimpleCache) get(key interface{}, onLoad bool) (interface{}, error) { // 內部方法根據key獲取值 v, err := c.getValue(key, onLoad) if err != nil { return nil, err } if c.deserializeFunc != nil { return c.deserializeFunc(key, v) } return v, nil } // 內部獲取方法 // 1. 加鎖 // 2. 判斷是否過時 若是過時直接刪除數據 // 3. 若是沒有過時則返回數據 增長hit基數器 // 4. 若是沒有命中 增長MissCount func (c *SimpleCache) getValue(key interface{}, onLoad bool) (interface{}, error) { c.mu.Lock() item, ok := c.items[key] if ok { if !item.IsExpired(nil) { v := item.value c.mu.Unlock() if !onLoad { c.stats.IncrHitCount() } return v, nil } c.remove(key) } c.mu.Unlock() if !onLoad { c.stats.IncrMissCount() } return nil, KeyNotFoundError }
LRU在以前已經介紹過了,意思是最近最少使用。LRU Cache 的替換原則就是將最近最少使用的內容替換掉。
gcache實現的方法是經過鏈表來實現這個策略。當每次get或者set以後則把這個節點放到鏈表的頭部,當須要超過size時則刪除鏈表尾部的節點數據。這樣就實現了最近最少使用的策略。緩存
type LRUCache struct { baseCache items map[interface{}]*list.Element evictList *list.List } type lruItem struct { clock Clock key interface{} value interface{} expiration *time.Time }
// 先加鎖防止多線程修改數據,調用內部set方法設置數據。 func (c *LRUCache) Set(key, value interface{}) error { c.mu.Lock() defer c.mu.Unlock() _, err := c.set(key, value) return err } // 內部設置數據方法 func (c *LRUCache) set(key, value interface{}) (interface{}, error) { var err error // 判斷執行序列化回調函數 if c.serializeFunc != nil { value, err = c.serializeFunc(key, value) if err != nil { return nil, err } } // Check for existing item var item *lruItem // 從items map中獲取值 if it, ok := c.items[key]; ok { // 若是key本來就存在,則從新設置而後移動節點到鏈表的頭部 c.evictList.MoveToFront(it) item = it.Value.(*lruItem) item.value = value } else { // 若是超過size則調用evict函數根據LRU策略去除緩存中的一個數據 if c.evictList.Len() >= c.size { c.evict(1) } // 建立對象而後放入鏈表和items中 item = &lruItem{ clock: c.clock, key: key, value: value, } c.items[key] = c.evictList.PushFront(item) } // 判斷是否有過時時間 有則設置 if c.expiration != nil { t := c.clock.Now().Add(*c.expiration) item.expiration = &t } // 判斷調用 added回調函數 if c.addedFunc != nil { c.addedFunc(key, value) } return item, nil } // 驅逐函數 func (c *LRUCache) evict(count int) { // 循環刪除鏈表尾部的節點 for i := 0; i < count; i++ { ent := c.evictList.Back() if ent == nil { return } else { c.removeElement(ent) } } }
LFU:意思是最近最不經常使用。LFU Cache先淘汰必定時間內被訪問次數最少的頁面。安全
LFU策略,淘汰的是訪問次數最少的,意味着cache須要保存每一個緩存數據的訪問次數。但如何保存訪問次數呢,咱們能夠看下面的結構體定義。數據結構
items map[interface{}]*lfuItem :保存數據,保證訪問時候的高效
lfuItem:保存在map中,其中存放這key、value、過時時間、一個鏈表節點的地址。這個地址用來方便操做鏈表中的數據。
freqList:鏈表結構,保存freqEntry
freqEntry:包含兩個字段一個是freq用來保存訪問次數,另外一個是items map類型用來保存次訪問次數的具體數據,能夠是多個
gcache的LFU使用一個map來保存數據 一個鏈表(包含次數和map)來保存緩存中數據被訪問的次數。初次set時訪問次數默認爲0。若是淘汰則是淘汰被訪問次數最少的,則能夠從鏈表的頭部開始掃描,一直找到最少的。多線程
圖一 是set5個字符串到cache中,5個字符串不重複。items中的數據咱們不看只畫了鏈表中的數據狀態。
這個時候鏈表中只有一個節點,這個節點數據中的freq爲0,意味着這個節點中的數據都是沒有被訪問的。併發
圖二 是通過幾回get和一次set操做後的鏈表數據結果。能夠看到鏈表的每個節點都表明着一個訪問次數而且依次遞增。
每次get訪問數據時候經過上面提到的lfuItem中的指針獲取到節點在鏈表所在的位置,把數據日後移動一個節點。若是沒有節點測建立一個以此類推。那麼獲得的結果就是越靠近頭部的數據訪問次數是最少的。若是淘汰則優先淘汰這些數據。函數
type LFUCache struct { baseCache items map[interface{}]*lfuItem freqList *list.List // list for freqEntry } type freqEntry struct { freq uint items map[*lfuItem]struct{} } type lfuItem struct { clock Clock key interface{} value interface{} freqElement *list.Element expiration *time.Time }
func (c *LFUCache) Set(key, value interface{}) error { c.mu.Lock() defer c.mu.Unlock() _, err := c.set(key, value) return err } // set內部方法 func (c *LFUCache) set(key, value interface{}) (interface{}, error) { var err error if c.serializeFunc != nil { value, err = c.serializeFunc(key, value) if err != nil { return nil, err } } // 檢查key是否存在 item, ok := c.items[key] if ok { // 存在則直接賦值 item.value = value } else { // 不存在而且數量超出則執行驅逐函數 if len(c.items) >= c.size { c.evict(1) } // 新建item對象 item = &lfuItem{ clock: c.clock, key: key, value: value, freqElement: nil, } // 把新建的lfuitem對象放到鏈表第一個節點中 el := c.freqList.Front() fe := el.Value.(*freqEntry) fe.items[item] = struct{}{} item.freqElement = el c.items[key] = item } if c.expiration != nil { t := c.clock.Now().Add(*c.expiration) item.expiration = &t } if c.addedFunc != nil { c.addedFunc(key, value) } return item, nil } // 驅逐函數 func (c *LFUCache) evict(count int) { // 獲取鏈表第一個節點 entry := c.freqList.Front() // 循環count for i := 0; i < count; { if entry == nil { return } else { // 循環判斷啊鏈表節點中是否有數據 若是沒有則調用next 繼續循環 for item, _ := range entry.Value.(*freqEntry).items { if i >= count { return } c.removeItem(item) i++ } entry = entry.Next() } } }
func (c *LFUCache) get(key interface{}, onLoad bool) (interface{}, error) { v, err := c.getValue(key, onLoad) if err != nil { return nil, err } if c.deserializeFunc != nil { return c.deserializeFunc(key, v) } return v, nil } // 判斷是否過時,若是沒過時則獲取而且執行increment函數操做鏈表 func (c *LFUCache) getValue(key interface{}, onLoad bool) (interface{}, error) { c.mu.Lock() item, ok := c.items[key] if ok { if !item.IsExpired(nil) { c.increment(item) v := item.value c.mu.Unlock() if !onLoad { c.stats.IncrHitCount() } return v, nil } c.removeItem(item) } c.mu.Unlock() if !onLoad { c.stats.IncrMissCount() } return nil, KeyNotFoundError } // 將lfuItem 放入下一個節點中的map中,若是沒有則建立一個新的lfuItem func (c *LFUCache) increment(item *lfuItem) { currentFreqElement := item.freqElement currentFreqEntry := currentFreqElement.Value.(*freqEntry) nextFreq := currentFreqEntry.freq + 1 delete(currentFreqEntry.items, item) nextFreqElement := currentFreqElement.Next() if nextFreqElement == nil { nextFreqElement = c.freqList.InsertAfter(&freqEntry{ freq: nextFreq, items: make(map[*lfuItem]struct{}), }, currentFreqElement) } nextFreqElement.Value.(*freqEntry).items[item] = struct{}{} item.freqElement = nextFreqElement }
ARC:Adaptive Replacement Cache,ARC介於 LRU 和 LFU 之間。源碼分析
ARC是介於LRU和LFU之間的算法。也是經過map來存儲數據,保證存取的性能。那是如何實現LRU和LFU又是如何平衡兩個策略的呢?
結構體能夠參看下面的代碼:
items
: map數據結構保存key,value則是arcItem結構體,其中包含了key、value、過時時間。注意其中沒有像LFU的鏈表指針。t1
:LRU策略,set以後會放入t1中限制數量跟整個cache數量相同。t2
:LFU策略,當get訪問以後會從t1移動到t2之中,不過不管訪問幾回都會在t2之中,不像LFU同樣會記錄訪問次數。b1
:接收t1(LRU)策略淘汰的緩存數據。若是超過size則直接從cache中刪除。b2
:接收t2(LFU)策略淘汰的緩存數據。跟b1同樣超過size也會從cache中刪除。那每次Set、Get數據又是怎麼流動的呢?下面圖解:
圖一:是初始化而且添加5條數據以後cache內部數據結構。items保存所有數據,由於沒有訪問數據則全部數據都會放到t1中。
圖二:獲取了aaa、bbb、ddd、eee
4個數據,而後有set了fff到cache中。假設這個cache的size爲5。
其中aaa、bbb、ddd、eee
被移動到了t2中,剩下的ccc沒有訪問則會繼續保留再t1之中。可是最後一條語句又設置了fff
到cache中。發現size已經滿則須要淘汰一個數據,則會淘汰t1中的數據ccc移動到b1中。items之中則沒有ccc數據了。
最終的數據流動以下圖:
type ARC struct { baseCache items map[interface{}]*arcItem part int t1 *arcList t2 *arcList b1 *arcList b2 *arcList } type arcItem struct { clock Clock key interface{} value interface{} expiration *time.Time } type arcList struct { l *list.List keys map[interface{}]*list.Element }
func (c *ARC) Set(key, value interface{}) error { c.mu.Lock() defer c.mu.Unlock() _, err := c.set(key, value) return err } // 1. 判斷緩存中是否有數據 // 2. 在b1,b2中查看是否存在,若是存在則刪除b1 b2 從新放入到t2中 // 3. func (c *ARC) set(key, value interface{}) (interface{}, error) { var err error if c.serializeFunc != nil { value, err = c.serializeFunc(key, value) if err != nil { return nil, err } } item, ok := c.items[key] if ok { item.value = value } else { item = &arcItem{ clock: c.clock, key: key, value: value, } c.items[key] = item } if c.expiration != nil { t := c.clock.Now().Add(*c.expiration) item.expiration = &t } defer func() { if c.addedFunc != nil { c.addedFunc(key, value) } }() if c.t1.Has(key) || c.t2.Has(key) { return item, nil } if elt := c.b1.Lookup(key); elt != nil { c.setPart(minInt(c.size, c.part+maxInt(c.b2.Len()/c.b1.Len(), 1))) c.replace(key) c.b1.Remove(key, elt) c.t2.PushFront(key) return item, nil } if elt := c.b2.Lookup(key); elt != nil { c.setPart(maxInt(0, c.part-maxInt(c.b1.Len()/c.b2.Len(), 1))) c.replace(key) c.b2.Remove(key, elt) c.t2.PushFront(key) return item, nil } if c.isCacheFull() && c.t1.Len()+c.b1.Len() == c.size { if c.t1.Len() < c.size { c.b1.RemoveTail() c.replace(key) } else { pop := c.t1.RemoveTail() item, ok := c.items[pop] if ok { delete(c.items, pop) if c.evictedFunc != nil { c.evictedFunc(item.key, item.value) } } } } else { total := c.t1.Len() + c.b1.Len() + c.t2.Len() + c.b2.Len() if total >= c.size { if total == (2 * c.size) { if c.b2.Len() > 0 { c.b2.RemoveTail() } else { c.b1.RemoveTail() } } c.replace(key) } } c.t1.PushFront(key) return item, nil }
若是t1中存在則從t1移動到t2,若是存在再t2之中則放到t2的頭部節點。
func (c *ARC) Get(key interface{}) (interface{}, error) { v, err := c.get(key, false) if err == KeyNotFoundError { return c.getWithLoader(key, true) } return v, err } func (c *ARC) get(key interface{}, onLoad bool) (interface{}, error) { v, err := c.getValue(key, onLoad) if err != nil { return nil, err } if c.deserializeFunc != nil { return c.deserializeFunc(key, v) } return v, nil } func (c *ARC) getValue(key interface{}, onLoad bool) (interface{}, error) { c.mu.Lock() defer c.mu.Unlock() if elt := c.t1.Lookup(key); elt != nil { c.t1.Remove(key, elt) item := c.items[key] if !item.IsExpired(nil) { c.t2.PushFront(key) if !onLoad { c.stats.IncrHitCount() } return item.value, nil } else { delete(c.items, key) c.b1.PushFront(key) if c.evictedFunc != nil { c.evictedFunc(item.key, item.value) } } } if elt := c.t2.Lookup(key); elt != nil { item := c.items[key] if !item.IsExpired(nil) { c.t2.MoveToFront(elt) if !onLoad { c.stats.IncrHitCount() } return item.value, nil } else { delete(c.items, key) c.t2.Remove(key, elt) c.b2.PushFront(key) if c.evictedFunc != nil { c.evictedFunc(item.key, item.value) } } } if !onLoad { c.stats.IncrMissCount() } return nil, KeyNotFoundError }
自此gcache全部的策略都已經分析完了。看完分析能夠看出來gcache支持的策略不少,而且使用十分簡單。只要在聲明的時候肯定好策略就可使用對應的策略。更加支持各類回調函數,讓邏輯更加靈活複合各類需求。寫這篇文章也在網上找了一些資料,可是都不是特別的詳細因此不停的調試和畫圖分析出來的結果。但願能對你們能有所幫助。