本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库
本地缓存是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技术。
使用本地缓存的优势:
下面我们从简单到复杂写本地缓存
在 Go 中,你可以使用内置的 sync 包和 map 数据结构来实现本地缓存。
我们首先定义了一个名为 Cache 的结构体,其中包含一个 data 字段,它是一个 map[string]interface{}
类型的数据结构,用于存储键值对。我们使用 sync.RWMutex
来保证并发安全性。
然后,我们定义了 Set 方法和 Get 方法,用于设置和获取缓存值。在 Set 方法中,我们使用互斥锁 mu 来保证并发安全。在 Get 方法中,我们使用读写锁 mu 的读锁来实现并发读取。
package cache
import (
"sync"
)
type CacheNormal struct {
data map[string]interface{}
mu sync.RWMutex
}
func NewCache() *CacheNormal {
return &CacheNormal{
data: make(map[string]interface{}),
}
}
func (c *CacheNormal) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *CacheNormal) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.data[key]
return value, ok
}
代码测试:
package cache
import (
"fmt"
"testing"
"time"
)
func TestCacheNorm(t *testing.T) {
cache := NewCache()
// 设置缓存值
cache.Set("key1", "value1")
cache.Set("key2", "value2")
// 读取缓存值
value1, ok1 := cache.Get("key1")
fmt.Println("Key1:", value1, ok1)
value2, ok2 := cache.Get("key2")
fmt.Println("Key2:", value2, ok2)
// 等待一段时间
time.Sleep(5 * time.Second)
// 再次读取缓存值
value1, ok1 = cache.Get("key1")
fmt.Println("Key1:", value1, ok1)
value2, ok2 = cache.Get("key2")
fmt.Println("Key2:", value2, ok2)
}
要实现带有过期时间的本地缓存,可以使用 Go 的 sync 包和 map 数据结构结合定时器(time.Timer)来实现。
我们定义了一个名为 CacheEx 的结构体,其中包含了一个用于存储缓存项的 data 字段,并且还有一个用于接收过期键的通道 expireCh。
通过调用 NewCacheEx 函数创建一个新的缓存对象,该函数会启动一个协程 startCleanup 来定期清理过期的缓存项。
使用 Set 方法来设置缓存值,并指定缓存项的过期时间。在这个方法中,我们使用互斥锁来保证并发安全性,并将缓存项的过期时间和值存储在 data 中。同时,我们还使用 scheduleExpiration 方法来安排过期时的清理操作。
使用 Get 方法来获取缓存值。在这个方法中,我们使用读锁来进行并发读取,并检查缓存项是否过期。如果缓存项存在且未过期,则返回对应的值;否则返回空值。
package cache
import (
"sync"
"time"
)
type CacheEx struct {
data map[string]cacheItem
mu sync.RWMutex
expireCh chan string
}
type cacheItem struct {
value interface{}
expiration time.Time
}
func NewCacheEx() *CacheEx {
c := &CacheEx{
data: make(map[string]cacheItem),
expireCh: make(chan string),
}
go c.startCleanup()
return c
}
func (c *CacheEx) Set(key string, value interface{}, expiration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
expireTime := time.Now().Add(expiration)
c.data[key] = cacheItem{
value: value,
expiration: expireTime,
}
go c.scheduleExpiration(key, expireTime)
}
func (c *CacheEx) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.data[key]
if ok && item.expiration.After(time.Now()) {
return item.value, true
}
return nil, false
}
func (c *CacheEx) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
func (c *CacheEx) startCleanup() {
for {
key := <-c.expireCh
c.Delete(key)
}
}
func (c *CacheEx) scheduleExpiration(key string, expireTime time.Time) {
duration := time.Until(expireTime)
timer := time.NewTimer(duration)
<-timer.C
c.expireCh <- key
}
代码测试:
func TestCacheExpireTime(t *testing.T) {
cache := NewCacheEx()
// 设置缓存值,带有过期时间
cache.Set("key1", "value1", 2*time.Second)
cache.Set("key2", "value2", 5*time.Second)
// 读取缓存值
value1, ok1 := cache.Get("key1")
fmt.Println("Key1:", value1, ok1)
value2, ok2 := cache.Get("key2")
fmt.Println("Key2:", value2, ok2)
// 等待一段时间
time.Sleep(3 * time.Second)
// 再次读取缓存值
value1, ok1 = cache.Get("key1")
fmt.Println("Key1:", value1, ok1)
value2, ok2 = cache.Get("key2")
fmt.Println("Key2:", value2, ok2)
}
package cache
import (
"sync"
"time"
)
type item struct {
value interface{}
expiration int64
}
type CacheV3 struct {
items sync.Map
lock sync.RWMutex
defaultTTL time.Duration
maxCapacity int
evictList []interface{}
}
func NewCacheV3(defaultTTL time.Duration, maxCapacity int) *CacheV3 {
return &CacheV3{
defaultTTL: defaultTTL,
maxCapacity: maxCapacity,
evictList: make([]interface{}, 0, maxCapacity),
}
}
func (c *CacheV3) Set(key string, value interface{}, ttl time.Duration) {
c.lock.Lock()
defer c.lock.Unlock()
if c.cacheSize() >= c.maxCapacity {
c.evict(1)
}
if ttl == 0 {
ttl = c.defaultTTL
}
expiration := time.Now().Add(ttl).UnixNano()
c.items.Store(key, &item{value, expiration})
time.AfterFunc(ttl, func() {
c.lock.Lock()
defer c.lock.Unlock()
if _, found := c.items.Load(key); found {
c.items.Delete(key)
c.evictList = append(c.evictList, key)
}
})
}
func (c *CacheV3) Get(key string) (interface{}, bool) {
c.lock.RLock()
defer c.lock.RUnlock()
if val, found := c.items.Load(key); found {
item := val.(*item)
if item.expiration > 0 && time.Now().UnixNano() > item.expiration {
c.items.Delete(key)
return nil, false
}
return item.value, true
}
return nil, false
}
func (c *CacheV3) evict(count int) {
for i := 0; i < count; i++ {
key := c.evictList[0]
c.evictList = c.evictList[1:]
c.items.Delete(key)
}
}
func (c *CacheV3) cacheSize() int {
size := 0
c.items.Range(func(_, _ interface{}) bool {
size++
return true
})
return size
}
代码测试:
func TestCacheV3(t *testing.T) {
c := NewCacheV3(time.Minute, 100)
c.Set("key1", "value1", time.Second*30)
c.Set("key2", "value2", time.Minute)
val, found := c.Get("key1")
if found {
fmt.Println(val)
}
time.Sleep(time.Second * 45)
val, found = c.Get("key1")
if found {
fmt.Println(val)
}
time.Sleep(time.Second * 30)
val, found = c.Get("key1")
if found {
fmt.Println(val)
} else {
fmt.Println("key1 expired")
}
}
最新代码请参考:https://github.com/muesli/cache2go
以下代码仅供参考
type Item struct {
//read write lock
sync.RWMutex
key interface{}
data interface{}
// cache duration.
duration time.Duration
// create time
createTime time.Time
//last access time
accessTime time.Time
//visit times
count int64
// callback after deleting
deleteCallback func(key interface{})
}
//create item.
func NewItem(key interface{}, duration time.Duration, data interface{}) *Item {
t := time.Now()
return &Item{
key: key,
duration: duration,
createTime: t,
accessTime: t,
count: 0,
deleteCallback: nil,
data: data,
}
}
//keep alive
func (item *Item) KeepAlive() {
item.Lock()
defer item.Unlock()
item.accessTime = time.Now()
item.count++
}
func (item *Item) Duration() time.Duration {
return item.duration
}
func (item *Item) AccessTime() time.Time {
item.RLock()
defer item.RUnlock()
return item.accessTime
}
func (item *Item) CreateTime() time.Time {
return item.createTime
}
func (item *Item) Count() int64 {
item.RLock()
defer item.RUnlock()
return item.count
}
func (item *Item) Key() interface{} {
return item.key
}
func (item *Item) Data() interface{} {
return item.data
}
func (item *Item) SetDeleteCallback(f func(interface{})) {
item.Lock()
defer item.Unlock()
item.deleteCallback = f
}
// table for managing cache items
type Table struct {
sync.RWMutex
//all cache items
items map[interface{}]*Item
// trigger cleanup
cleanupTimer *time.Timer
// cleanup interval
cleanupInterval time.Duration
loadData func(key interface{}, args ...interface{}) *Item
// callback after adding.
addedCallback func(item *Item)
// callback after deleting
deleteCallback func(item *Item)
}
func (table *Table) Count() int {
table.RLock()
defer table.RUnlock()
return len(table.items)
}
func (table *Table) Foreach(trans func(key interface{}, item *Item)) {
table.RLock()
defer table.RUnlock()
for k, v := range table.items {
trans(k, v)
}
}
func (table *Table) SetDataLoader(f func(interface{}, ...interface{}) *Item) {
table.Lock()
defer table.Unlock()
table.loadData = f
}
func (table *Table) SetAddedCallback(f func(*Item)) {
table.Lock()
defer table.Unlock()
table.addedCallback = f
}
func (table *Table) SetDeleteCallback(f func(*Item)) {
table.Lock()
defer table.Unlock()
table.deleteCallback = f
}
func (table *Table) RunWithRecovery(f func()) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("occur error %v \r\n", err)
}
}()
f()
}
func (table *Table) checkExpire() {
table.Lock()
if table.cleanupTimer != nil {
table.cleanupTimer.Stop()
}
if table.cleanupInterval > 0 {
table.log("Expiration check triggered after %v for table", table.cleanupInterval)
} else {
table.log("Expiration check installed for table")
}
// in order to not take the lock. use temp items.
items := table.items
table.Unlock()
//in order to make timer more precise, update now every loop.
now := time.Now()
smallestDuration := 0 * time.Second
for key, item := range items {
//take out our things, in order not to take the lock.
item.RLock()
duration := item.duration
accessTime := item.accessTime
item.RUnlock()
// 0 means valid.
if duration == 0 {
continue
}
if now.Sub(accessTime) >= duration {
//cache item expired.
_, e := table.Delete(key)
if e != nil {
table.log("occur error while deleting %v", e.Error())
}
} else {
//find the most possible expire item.
if smallestDuration == 0 || duration-now.Sub(accessTime) < smallestDuration {
smallestDuration = duration - now.Sub(accessTime)
}
}
}
//trigger next clean
table.Lock()
table.cleanupInterval = smallestDuration
if smallestDuration > 0 {
table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
go table.RunWithRecovery(table.checkExpire)
})
}
table.Unlock()
}
// add item
func (table *Table) Add(key interface{}, duration time.Duration, data interface{}) *Item {
item := NewItem(key, duration, data)
table.Lock()
table.log("Adding item with key %v and lifespan of %d to table", key, duration)
table.items[key] = item
expDur := table.cleanupInterval
addedItem := table.addedCallback
table.Unlock()
if addedItem != nil {
addedItem(item)
}
//find the most possible expire item.
if duration > 0 && (expDur == 0 || duration < expDur) {
table.checkExpire()
}
return item
}
func (table *Table) Delete(key interface{}) (*Item, error) {
table.RLock()
r, ok := table.items[key]
if !ok {
table.RUnlock()
return nil, errors.New(fmt.Sprintf("no item with key %s", key))
}
deleteCallback := table.deleteCallback
table.RUnlock()
if deleteCallback != nil {
deleteCallback(r)
}
r.RLock()
defer r.RUnlock()
if r.deleteCallback != nil {
r.deleteCallback(key)
}
table.Lock()
defer table.Unlock()
table.log("Deleting item with key %v created on %s and hit %d times from table", key, r.createTime, r.count)
delete(table.items, key)
return r, nil
}
//check exist.
func (table *Table) Exists(key interface{}) bool {
table.RLock()
defer table.RUnlock()
_, ok := table.items[key]
return ok
}
//if exist, return false. if not exist add a key and return true.
func (table *Table) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool {
table.Lock()
if _, ok := table.items[key]; ok {
table.Unlock()
return false
}
item := NewItem(key, lifeSpan, data)
table.log("Adding item with key %v and lifespan of %d to table", key, lifeSpan)
table.items[key] = item
expDur := table.cleanupInterval
addedItem := table.addedCallback
table.Unlock()
if addedItem != nil {
addedItem(item)
}
if lifeSpan > 0 && (expDur == 0 || lifeSpan < expDur) {
table.checkExpire()
}
return true
}
func (table *Table) Value(key interface{}, args ...interface{}) (*Item, error) {
table.RLock()
r, ok := table.items[key]
loadData := table.loadData
table.RUnlock()
if ok {
//update visit count and visit time.
r.KeepAlive()
return r, nil
}
if loadData != nil {
item := loadData(key, args...)
if item != nil {
table.Add(key, item.duration, item.data)
return item, nil
}
return nil, errors.New("cannot load item")
}
return nil, nil
}
// truncate a table.
func (table *Table) Truncate() {
table.Lock()
defer table.Unlock()
table.log("Truncate table")
table.items = make(map[interface{}]*Item)
table.cleanupInterval = 0
if table.cleanupTimer != nil {
table.cleanupTimer.Stop()
}
}
//support table sort
type ItemPair struct {
Key interface{}
AccessCount int64
}
type ItemPairList []ItemPair
func (p ItemPairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p ItemPairList) Len() int { return len(p) }
func (p ItemPairList) Less(i, j int) bool { return p[i].AccessCount > p[j].AccessCount }
//return most visited.
func (table *Table) MostAccessed(count int64) []*Item {
table.RLock()
defer table.RUnlock()
p := make(ItemPairList, len(table.items))
i := 0
for k, v := range table.items {
p[i] = ItemPair{k, v.count}
i++
}
sort.Sort(p)
var r []*Item
c := int64(0)
for _, v := range p {
if c >= count {
break
}
item, ok := table.items[v.Key]
if ok {
r = append(r, item)
}
c++
}
return r
}
// print log.
func (table *Table) log(format string, v ...interface{}) {
//fmt.Printf(format+"\r\n", v)
}
func NewTable() *Table {
return &Table{
items: make(map[interface{}]*Item),
}
}
https://github.com/patrickmn/go-cache
优点:
缺点:
https://github.com/allegro/bigcache
优点:
缺点:
https://github.com/golang/groupcache
优点:
缺点:
参考文档:
https://zhuanlan.zhihu.com/p/487455942
https://www.jianshu.com/p/0ff2e8c61c9c?tdsourcetag=s_pctim_aiomsg