Go gcache 源碼分析(圖解)

概述

gcache是一個用go實現的併發安全的本地緩存庫。他可以實現如下功能:

  • 指定緩存的的大小,初始化之時爲cache設置size大小。
  • 支持多種緩存的策略:Simple、LRU、LFU、ARC
  1. Simple:最普通的緩存策略,根據先存入的先淘汰。
  2. LUR:Least Recently Used,意思是最近最少使用。LRU Cache 的替換原則就是將最近最少使用的內容替換掉。
  3. LFU:Least Frequently Used ,意思是最近最不常用。LFU Cache先淘汰一定時間內被訪問次數最少的頁面。
  4. ARC:Adaptive Replacement Cache,ARC介於 LRU 和 LFU 之間。
  • 支持多個回調函數
  1. LoaderExpireFunc:過期回調函數
  2. EvictedFunc:淘汰回調函數
  3. PurgeVisitorFunc:清除所有key回調函數
  4. AddedFunc:新怎key回調函數
  5. SerializeFunc:對value序列化回調函數
  6. DeserializeFunc:對value反序列化回調函數
  • 支持計數事件
  1. HitCount:命中次數
  2. MissCount:沒有命中的次數
  3. LookupCount:查找次數
  4. HitRate:命中率
  • 使用singleflight機制,多個人請求一個key,保證只有一個真正獲取數據其餘等待結果。

簡單使用

其實github上已經有了很詳細的例子,其中有簡單key/value、設置超時時間、設置淘汰策略、設置回調函數等各種例子。這裏簡單摘抄一些簡單的例子:

簡單key/value 設置

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

使用load回調函數

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類
// 緩存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
}
build 輸出cache對象
// 判斷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

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
}
Set方法
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方法

// 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
}

LRUCache

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方法
// 先加鎖防止多線程修改數據,調用內部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 Cache

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
}
Set方法
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()
        }
    }
}
Get方法
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 Cache

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、eee4個數據,然後有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
}
Set方法
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
}
Get方法

如果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支持的策略很多,並且使用十分簡單。只要在聲明的時候確定好策略就可以使用對應的策略。更加支持各種回調函數,讓邏輯更加靈活複合各種需求。
寫這篇文章也在網上找了一些資料,但是都不是特別的詳細所以不停的調試和畫圖分析出來的結果。希望能對大家能有所幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章