本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库
文章目录
- 前言
- 手写本地缓存
- CacheNormal
- CacheEx
- CacheV3
- 开源库
- cache2go
- go-cache
- bigcache
- groupcache
- 本地缓存对比
前言
本地缓存是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技术。
使用本地缓存的优势:
- 提高应用程序性能
- 减少网络延迟
- 改善用户体验
- 降低外部存储系统的负荷
下面我们从简单到复杂写本地缓存
手写本地缓存
CacheNormal
在 Go 中,你可以使用内置的 sync 包和 map 数据结构来实现本地缓存。
我们首先定义了一个名为 Cache 的结构体,其中包含一个 data 字段,它是一个 map[string]interface{}
类型的数据结构,用于存储键值对。我们使用 sync.RWMutex
来保证并发安全性。
然后,我们定义了 Set 方法和 Get 方法,用于设置和获取缓存值。在 Set 方法中,我们使用互斥锁 mu 来保证并发安全。在 Get 方法中,我们使用读写锁 mu 的读锁来实现并发读取。
package cacheimport ("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 cacheimport ("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)
}
结果展示:
下面我们实现一个带有过期时间的本地缓存。
CacheEx
要实现带有过期时间的本地缓存,可以使用 Go 的 sync 包和 map 数据结构结合定时器(time.Timer)来实现。
我们定义了一个名为 CacheEx 的结构体,其中包含了一个用于存储缓存项的 data 字段,并且还有一个用于接收过期键的通道 expireCh。
通过调用 NewCacheEx 函数创建一个新的缓存对象,该函数会启动一个协程 startCleanup 来定期清理过期的缓存项。
使用 Set 方法来设置缓存值,并指定缓存项的过期时间。在这个方法中,我们使用互斥锁来保证并发安全性,并将缓存项的过期时间和值存储在 data 中。同时,我们还使用 scheduleExpiration 方法来安排过期时的清理操作。
使用 Get 方法来获取缓存值。在这个方法中,我们使用读锁来进行并发读取,并检查缓存项是否过期。如果缓存项存在且未过期,则返回对应的值;否则返回空值。
package cacheimport ("sync""time"
)type CacheEx struct {data map[string]cacheItemmu sync.RWMutexexpireCh 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.expireChc.Delete(key)}
}func (c *CacheEx) scheduleExpiration(key string, expireTime time.Time) {duration := time.Until(expireTime)timer := time.NewTimer(duration)<-timer.Cc.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)
}
结果展示:
CacheV3
package cacheimport ("sync""time"
)type item struct {value interface{}expiration int64
}type CacheV3 struct {items sync.Maplock sync.RWMutexdefaultTTL time.DurationmaxCapacity intevictList []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 := 0c.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")}
}
结果展示:
开源库
cache2go
最新代码请参考:https://github.com/muesli/cache2go
以下代码仅供参考
type Item struct {//read write locksync.RWMutexkey interface{}data interface{}// cache duration.duration time.Duration// create timecreateTime time.Time//last access timeaccessTime time.Time//visit timescount int64// callback after deletingdeleteCallback 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 itemsitems map[interface{}]*Item// trigger cleanupcleanupTimer *time.Timer// cleanup intervalcleanupInterval time.DurationloadData func(key interface{}, args ...interface{}) *Item// callback after adding.addedCallback func(item *Item)// callback after deletingdeleteCallback 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.itemstable.Unlock()//in order to make timer more precise, update now every loop.now := time.Now()smallestDuration := 0 * time.Secondfor key, item := range items {//take out our things, in order not to take the lock.item.RLock()duration := item.durationaccessTime := item.accessTimeitem.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 cleantable.Lock()table.cleanupInterval = smallestDurationif 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] = itemexpDur := table.cleanupIntervaladdedItem := table.addedCallbacktable.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.deleteCallbacktable.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] = itemexpDur := table.cleanupIntervaladdedItem := table.addedCallbacktable.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.loadDatatable.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 = 0if table.cleanupTimer != nil {table.cleanupTimer.Stop()}
}//support table sort
type ItemPair struct {Key interface{}AccessCount int64
}type ItemPairList []ItemPairfunc (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 := 0for k, v := range table.items {p[i] = ItemPair{k, v.count}i++}sort.Sort(p)var r []*Itemc := 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),}
}
go-cache
https://github.com/patrickmn/go-cache
-
优点:
- 简单易用,适合快速集成到现有项目中。
- 支持过期时间,可以自动淘汰过期的缓存项。
- 支持多种数据类型的缓存。
-
缺点:
- 性能略低于其他库,不适合高并发读写的场景。
- 不支持分布式缓存。
bigcache
https://github.com/allegro/bigcache
-
优点:
- 高性能,适用于需要快速读写大量数据的场景。
- 使用murmurhash算法来计算哈希值,减少了哈希冲突。
- 使用多个shard来减少锁竞争。
-
缺点:
- 不支持过期时间,只能手动清除过期的缓存项。
- 内存使用较高,不适合存储大量数据。
groupcache
https://github.com/golang/groupcache
-
优点:
- 支持分布式缓存,可以在多台机器上共享缓存。
- 采用LRU算法来淘汰缓存项,具备一定的缓存性能。
- 提供一致性哈希算法,可以解决节点扩容等问题。
-
缺点:
- 比较复杂,使用起来较为繁琐。
- 只支持字符串类型的键值对。
本地缓存对比
参考文档:
-
https://zhuanlan.zhihu.com/p/487455942
-
https://www.jianshu.com/p/0ff2e8c61c9c?tdsourcetag=s_pctim_aiomsg
下面对每个库的详细介绍:
- go-cache:
- 描述:go-cache是一款简单而有效的内存缓存库,支持设置过期时间和GC机制。
- 并发安全:是,使用Go的sync.Map实现数据的并发安全存储和访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:默认为LRU(最近最少使用)算法,也支持手动删除过期的缓存项。
- 分布式支持:不支持。
- freecache:
- 描述:freecache是一款高性能的内存缓存库,使用LRU算法进行缓存项的淘汰。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:固定大小,需要在初始化时指定总共可以缓存的字节数。
- 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
- 分布式支持:不支持。
- bigcache:
- 描述:bigcache是一款高性能的内存缓存库,使用murmurhash哈希算法快速查找。
- 并发安全:是,使用多个读写锁来实现高并发的访问控制。
- 存储限制:固定大小,需要在初始化时指定最多可以缓存的条目数。
- 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
- 分布式支持:不支持。
- groupcache:
- 描述:groupcache是一款支持分布式缓存的库,提供一致性哈希和HTTP请求缓存功能。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:支持自定义淘汰策略,例如手动删除过期的缓存项。
- 分布式支持:是,支持分布式缓存,将数据分片存储在多个节点上,通过查询一致性哈希环来确定数据所在的节点。
- gocache:
- 描述:gocache是一款快速、强大的内存缓存库,支持过期时间、并发安全和自定义淘汰策略。
- 并发安全:是,使用读写锁实现并发安全访问。
- 存储限制:无,可以存储任意类型的数据。
- 淘汰策略:默认为LRU(最近最少使用)算法,也支持自定义淘汰策略。
- 分布式支持:不支持。