Go gcache 源碼分析
【導讀】本文對本地緩存庫 gocache 做了詳細分析。
gcache 是一個用 go 實現的併發安全的本地緩存庫。他可以實現如下功能:
-
指定緩存的的大小,初始化之時爲 cache 設置 size 大小。
-
支持多種緩存的策略:Simple、LRU、LFU、ARC
-
Simple:最普通的緩存策略,根據先存入的先淘汰。
-
LUR:Least Recently Used,意思是最近最少使用。LRU Cache 的替換原則就是將最近最少使用的內容替換掉。
-
LFU:Least Frequently Used ,意思是最近最不常用。LFU Cache 先淘汰一定時間內被訪問次數最少的頁面。
-
ARC:Adaptive Replacement Cache,ARC 介於 LRU 和 LFU 之間。
- 支持多個回調函數
-
LoaderExpireFunc:過期回調函數
-
EvictedFunc:淘汰回調函數
-
PurgeVisitorFunc:清除所有 key 回調函數
-
AddedFunc:新怎 key 回調函數
-
SerializeFunc:對 value 序列化回調函數
-
DeserializeFunc:對 value 反序列化回調函數
- 支持計數事件
-
HitCount:命中次數
-
MissCount:沒有命中的次數
-
LookupCount:查找次數
-
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、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
}
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 支持的策略很多,並且使用十分簡單。只要在聲明的時候確定好策略就可以使用對應的策略。更加支持各種回調函數,讓邏輯更加靈活複合各種需求。
寫這篇文章也在網上找了一些資料,但是都不是特別的詳細所以不停的調試和畫圖分析出來的結果。希望能對大家能有所幫助。
轉自:
segmentfault.com/a/1190000020002827
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/NnY_0T6pkzOL3hG9kl-ZfA