Golang中本地緩存庫cache2go的使用小結
1.背景
1.1.項目介紹
cache2go是一款由golang實現(xiàn)的本地緩存庫,提供并發(fā)安全的讀寫操作,具有過期時間控制等特性。項目地址:https://github.com/muesli/cache2go
1.2.使用方法
go get github.com/muesli/cache2go
- 核心操作API:
- Cache:創(chuàng)建Cache Table
- Add:添加Cache
- Value:讀取Cache
- Delete:刪除指定Key Cache
- Flush:清空整個Cache Table
package main
import (
"github.com/muesli/cache2go"
"log"
"time"
)
type Item struct {
Name string `json:"name"`
Prices int64 `json:"prices"`
Stocks int64 `json:"stocks"`
}
func basicOpTest() {
// 初始化itemCache本地緩存
itemCache := cache2go.Cache("itemCache")
item := &Item{
Name: "MacBookPro",
Prices: 10000,
Stocks: 1,
}
// 添加item1緩存,過期時間為5秒鐘
itemCache.Add("item1", 5*time.Second, item)
// 讀取item1緩存
if v, err := itemCache.Value("item1"); err != nil {
log.Printf("item1 err = %v", err)
} else {
log.Printf("讀取item1緩存:%#v", v.Data())
}
// 睡眠6s后讀取
time.Sleep(6 * time.Second)
if v, err := itemCache.Value("item1"); err != nil {
log.Printf("item1 err = %v", err)
} else {
log.Printf("6s后讀取item1緩存:%#v", v.Data())
}
// 添加item2,不設置過期時間
itemCache.Add("item2", 0, item)
// 讀取item2緩存
if v, err := itemCache.Value("item2"); err != nil {
log.Printf("item2 err = %v", err)
} else {
log.Printf("讀取item2緩存:%#v", v.Data())
}
// 刪除掉item2緩存
itemCache.Delete("item2")
// 再讀取item2緩存
if v, err := itemCache.Value("item2"); err != nil {
log.Printf("item2 err = %v", err)
} else {
log.Printf("讀取item2緩存:%#v", v.Data())
}
// 添加item3緩存,并刪除所有緩存
itemCache.Add("item3", 0, item)
itemCache.Flush()
// 讀取item3緩存
if v, err := itemCache.Value("item3"); err != nil {
log.Printf("item3 err = %v", err)
} else {
log.Printf("讀取item3緩存:%#v", v.Data())
}
}運行結果:
2022/10/17 20:52:00 讀取item1緩存:&main.Item{Name:"MacBookPro", Prices:10000, Stocks:1}
2022/10/17 20:52:06 item1 err = Key not found in cache
2022/10/17 20:52:06 讀取item2緩存:&main.Item{Name:"MacBookPro", Prices:10000, Stocks:1}
2022/10/17 20:52:06 item2 err = Key not found in cache
2022/10/17 20:52:06 item3 err = Key not found in cache
- 操作回調API:
- AddAddedItemCallback:新增Cache回調函數(shù)
- AddAboutToDeleteItemCallback:刪除Cache回調函數(shù)
- AddAboutToExpireCallback:過期CacheItem回調函數(shù)
- 以上三個方法分別再對應著RemoveXXX,表示刪去對應操作的全部回調函數(shù)
func callBackTest() {
// 初始化itemCache本地緩存
itemCache := cache2go.Cache("itemCache")
// 設置各操作回調函數(shù)
itemCache.AddAddedItemCallback(func(item *cache2go.CacheItem) {
log.Printf("added callback, item = %#v", item)
})
itemCache.AddAboutToDeleteItemCallback(func(item *cache2go.CacheItem) {
log.Printf("deleted callback, item = %#v", item)
})
item := itemCache.Add("expire_item", 1*time.Second, Item{
Name: "expire_item",
Prices: 1,
Stocks: 1,
})
item.AddAboutToExpireCallback(func(item interface{}) {
log.Printf("expired callback, item = %#v", item)
})
// 執(zhí)行基本操作
basicOpTest()
}輸出結果
2022/10/17 21:12:09 added callback, item = &cache2go.CacheItem{RWMutex:sync.RWMutex{w:sync.Mutex{state:0, sema:0x0}, writerSem:0x0, readerSem:0x0, readerCount:0, readerWait:0}, key:"item1", data:(*main.Item)(0xc00008c040), lifeSpan:5000000000, createdOn:time.Time{wall:0xc0cb730a55e5a2f8, ext:426392, loc:(*time.Location)(0x1187880)}, accessedOn:time.Time{wall:0xc0cb730a55e5a2f8, ext:426392, loc:(*time.Location)(0x1187880)}, accessCount:0, aboutToExpire:[]func(interface {})(nil)}
2022/10/17 21:12:09 讀取item1緩存:&main.Item{Name:"MacBookPro", Prices:10000, Stocks:1}
2022/10/17 21:12:10 deleted callback, item = &cache2go.CacheItem{RWMutex:sync.RWMutex{w:sync.Mutex{state:0, sema:0x0}, writerSem:0x0, readerSem:0x0, readerCount:0, readerWait:0}, key:"expire_item", data:main.Item{Name:"expire_item", Prices:1, Stocks:1}, lifeSpan:1000000000, createdOn:time.Time{wall:0xc0cb730a55e4d7d8, ext:374551, loc:(*time.Location)(0x1187880)}, accessedOn:time.Time{wall:0xc0cb730a55e4d7d8, ext:374551, loc:(*time.Location)(0x1187880)}, accessCount:0, aboutToExpire:[]func(interface {}){(func(interface {}))(0x10a0530)}}
2022/10/17 21:12:10 expired callback, item = "expire_item"
2022/10/17 21:12:14 deleted callback, item = &cache2go.CacheItem{RWMutex:sync.RWMutex{w:sync.Mutex{state:0, sema:0x0}, writerSem:0x0, readerSem:0x0, readerCount:0, readerWait:0}, key:"item1", data:(*main.Item)(0xc00008c040), lifeSpan:5000000000, createdOn:time.Time{wall:0xc0cb730a55e5a2f8, ext:426392, loc:(*time.Location)(0x1187880)}, accessedOn:time.Time{wall:0xc0cb730a55eaa820, ext:755728, loc:(*time.Location)(0x1187880)}, accessCount:1, aboutToExpire:[]func(interface {})(nil)}
// ...
- 設置自定義緩存加載器:SetDataLoader
func dataLoaderTest() {
// 初始化itemCache本地緩存
redisItemCache := cache2go.Cache("redisItemCache")
// 設置自定義的cache加載邏輯
redisItemCache.SetDataLoader(func(key interface{}, args ...interface{}) *cache2go.CacheItem {
// 如果是redis開頭的key,先從redis中獲取
if strings.HasPrefix(key.(string), "redis") {
return cache2go.NewCacheItem(key, 0, Item{
Name: "redis_item",
})
}
return nil
})
// 寫入一條數(shù)據(jù)
redisItemCache.Add("item1", 0, Item{
Name: "item1",
})
item1, _ := redisItemCache.Value("item1")
log.Printf("item1 = %#v", item1)
redisItem, _ := redisItemCache.Value("redis_item")
log.Printf("redisItem = %#v", redisItem)
}輸出結果
2022/10/17 21:59:37 item1 = &cache2go.CacheItem{RWMutex:sync.RWMutex{w:sync.Mutex{state:0, sema:0x0}, writerSem:0x0, readerSem:0x0, readerCount:0, readerWait:0}, key:"item1", data:main.Item{Name:"item1", Prices:0, Stocks:0}, lifeSpan:0, createdOn:time.Time{wall:0xc0cb75d2601954b8, ext:492934, loc:(*time.Location)(0x11858c0)}, accessedOn:time.Time{wall:0xc0cb75d260196840, ext:497913, loc:(*time.Location)(0x11858c0)}, accessCount:1, aboutToExpire:[]func(interface {})(nil)}
2022/10/17 21:59:37 redisItem = &cache2go.CacheItem{RWMutex:sync.RWMutex{w:sync.Mutex{state:0, sema:0x0}, writerSem:0x0, readerSem:0x0, readerCount:0, readerWait:0}, key:"redis_item", data:main.Item{Name:"redis_item", Prices:0, Stocks:0}, lifeSpan:0, createdOn:time.Time{wall:0xc0cb75d2601d34e8, ext:746274, loc:(*time.Location)(0x11858c0)}, accessedOn:time.Time{wall:0xc0cb75d2601d34e8, ext:746274, loc:(*time.Location)(0x11858c0)}, accessCount:0, aboutToExpire:[]func(interface {})(nil)}
2.源碼分析
2.1.項目結構

核心代碼文件為:
- cachetable.go:封裝了CacheTable結構體,實現(xiàn)Cache列表相關操作API
- cacheitem.go:封裝了CacheItem結構體,實現(xiàn)了Cache對象相關操作API
- cache.go:提供cache全局map,存儲了CacheTable結構體與table名稱映射
2.2.數(shù)據(jù)結構
- CacheTable
type CacheTable struct {
sync.RWMutex
// The table's name.
name string
// All cached items.
items map[interface{}]*CacheItem
// Timer responsible for triggering cleanup.
cleanupTimer *time.Timer
// Current timer duration.
cleanupInterval time.Duration
// The logger used for this table.
logger *log.Logger
// Callback method triggered when trying to load a non-existing key.
loadData func(key interface{}, args ...interface{}) *CacheItem
// Callback method triggered when adding a new item to the cache.
addedItem []func(item *CacheItem)
// Callback method triggered before deleting an item from the cache.
aboutToDeleteItem []func(item *CacheItem)
}- CacheItem
type CacheItem struct {
sync.RWMutex
// The item's key.
key interface{}
// The item's data.
data interface{}
// How long will the item live in the cache when not being accessed/kept alive.
lifeSpan time.Duration
// Creation timestamp.
createdOn time.Time
// Last access timestamp.
accessedOn time.Time
// How often the item was accessed.
accessCount int64
// Callback method triggered right before removing the item from the cache
aboutToExpire []func(key interface{})
}2.3.API代碼流程
1.Cache
位于cache.go文件,維護了全局CacheTable Map。
var (
// 全局cache map
cache = make(map[string]*CacheTable)
// cache map 讀寫鎖
mutex sync.RWMutex
)
// 從cache map中獲取對應的CacheTable,不存在則創(chuàng)建新的
func Cache(table string) *CacheTable {
// 先上讀鎖,獲取cacheTable
mutex.RLock()
t, ok := cache[table]
mutex.RUnlock()
// 不存在,則新建
if !ok {
// 寫操作需要上寫鎖
mutex.Lock()
t, ok = cache[table]
// 雙重校驗是否存在
if !ok {
// 不存在則新建cacheTable
t = &CacheTable{
name: table,
items: make(map[interface{}]*CacheItem),
}
cache[table] = t
}
mutex.Unlock()
}
return t
}2.Add
位于cachetable.go文件,是CacheTable結構體的方法之一,實現(xiàn)了添加KV緩存的邏輯。
func (table *CacheTable) Add(key interface{}, lifeSpan time.Duration, data interface{}) *CacheItem {
// 封裝一個item
item := NewCacheItem(key, lifeSpan, data)
// 鎖表,將item添加進去
table.Lock()
table.addInternal(item)
return item
}
func (table *CacheTable) addInternal(item *CacheItem) {
// 添加kv值到map中
table.items[item.key] = item
expDur := table.cleanupInterval
addedItem := table.addedItem
// 添加完成解除寫鎖
table.Unlock()
// 觸發(fā)Add回調函數(shù)
if addedItem != nil {
for _, callback := range addedItem {
callback(item)
}
}
// 如果一個item有設置過期時間,且比檢查失效間隔小,則進行過期key清理(懶加載思想,只有存在這類Key才會啟動清理,而不是定時任務)
if item.lifeSpan > 0 && (expDur == 0 || item.lifeSpan < expDur) {
table.expirationCheck()
}
}
func (table *CacheTable) expirationCheck() {
table.Lock()
if table.cleanupTimer != nil {
table.cleanupTimer.Stop()
}
if table.cleanupInterval > 0 {
table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name)
} else {
table.log("Expiration check installed for table", table.name)
}
now := time.Now()
smallestDuration := 0 * time.Second
for key, item := range table.items {
// 遍歷該table下的所有items
item.RLock()
lifeSpan := item.lifeSpan
accessedOn := item.accessedOn
item.RUnlock()
if lifeSpan == 0 {
continue
}
if now.Sub(accessedOn) >= lifeSpan {
// 該item已超出存活時間,刪除key
table.deleteInternal(key)
} else {
// 找到最小的需要過期的item,計算最優(yōu)時間間隔
if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
smallestDuration = lifeSpan - now.Sub(accessedOn)
}
}
}
// 在最優(yōu)時間間隔后啟動定時任務檢查table的過期key
table.cleanupInterval = smallestDuration
if smallestDuration > 0 {
table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
go table.expirationCheck()
})
}
table.Unlock()
}3.Value
Value用于讀取Key匹配的CacheItem。
func (table *CacheTable) Value(key interface{}, args ...interface{}) (*CacheItem, error) {
table.RLock()
// 先嘗試從items中獲取該item
r, ok := table.items[key]
loadData := table.loadData
table.RUnlock()
if ok {
// 如果存在,則更新該item的accessOn時間和accessCount計數(shù)
r.KeepAlive()
return r, nil
}
// 如果不存在,則從loadData自定義加載函數(shù)中嘗試獲取
if loadData != nil {
item := loadData(key, args...)
if item != nil {
// 如果自定義加載函數(shù)中存在該item,則添加到table中并返回
table.Add(key, item.lifeSpan, item.data)
return item, nil
}
return nil, ErrKeyNotFoundOrLoadable
}
return nil, ErrKeyNotFound
}4.Delete
Delete函數(shù)用于刪除指定Key的Item。
func (table *CacheTable) Delete(key interface{}) (*CacheItem, error) {
table.Lock()
defer table.Unlock()
return table.deleteInternal(key)
}
func (table *CacheTable) deleteInternal(key interface{}) (*CacheItem, error) {
// 判斷key是否存在
r, ok := table.items[key]
if !ok {
return nil, ErrKeyNotFound
}
aboutToDeleteItem := table.aboutToDeleteItem
table.Unlock()
// 先觸發(fā)刪除回調函數(shù)
if aboutToDeleteItem != nil {
for _, callback := range aboutToDeleteItem {
callback(r)
}
}
r.RLock()
defer r.RUnlock()
// 觸發(fā)item的過期回調函數(shù)
if r.aboutToExpire != nil {
for _, callback := range r.aboutToExpire {
callback(key)
}
}
table.Lock()
table.log("Deleting item with key", key, "created on", r.createdOn, "and hit", r.accessCount, "times from table", table.name)
// 將item從table中刪除
delete(table.items, key)
return r, nil
}5.Flush
清空整個table的cache。
func (table *CacheTable) Flush() {
table.Lock()
defer table.Unlock()
table.log("Flushing table", table.name)
// 直接將items重新初始化
table.items = make(map[interface{}]*CacheItem)
table.cleanupInterval = 0
if table.cleanupTimer != nil {
// 如果此時還有清理過期定時器,則終止其運行
table.cleanupTimer.Stop()
}
}3.總結
cache2go這個項目寫得很精簡,本質上就是使用到了map來作為本地緩存kv存儲結構,但是有一些值得學習的地方:
- 使用map時,由于其是非線程安全的,所以在并發(fā)場景下需要上鎖,可以選擇RWMutex讀寫鎖來控制并發(fā)的讀和串行寫,避免panic
- 對于需要清理過期Key的場景,如果使用定時任務定時遍歷整個集合來做清理,會耗費較多時間和資源,可以由寫入時判斷是否存在需要清理的Key,再啟動定時任務來做清理,避免頻繁遍歷
- 可以利用golang函數(shù)式的特性,方便地實現(xiàn)各操作回調函數(shù),比如添加、刪除、失效操作回調等
- 另外個人覺得這個項目可以優(yōu)化的空間:由于使用的是本地緩存,為了避免內存oom可以在創(chuàng)建時指定限制key的最大數(shù)量,以及在內存不足時的寫入策略(如直接報錯或者隨機清理掉一批Key等)。
到此這篇關于Golang中本地緩存庫cache2go的使用小結的文章就介紹到這了,更多相關Golang本地緩存庫cache2go內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Golang限流器time/rate設計與實現(xiàn)詳解
在?Golang?庫中官方給我們提供了限流器的實現(xiàn)golang.org/x/time/rate,它是基于令牌桶算法(Token?Bucket)設計實現(xiàn)的,下面我們就來看看他的具體使用吧2024-03-03

