本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库
文章目录
前言
本地缓存是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技术。
使用本地缓存的优势:
- 提高应用程序性能
- 减少网络延迟
- 改善用户体验
- 降低外部存储系统的负荷
下面我们从简单到复杂写本地缓存
手写本地缓存
CacheNormal
在 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
}funcNewCache()*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")funcTestCacheNorm(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 cache
import("sync""time")type CacheEx struct{
data map[string]cacheItem
mu sync.RWMutex
expireCh chanstring}type cacheItem struct{
value interface{}
expiration time.Time
}funcNewCacheEx()*CacheEx {
c :=&CacheEx{
data:make(map[string]cacheItem),
expireCh:make(chanstring),}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}returnnil,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
}
代码测试:
funcTestCacheExpireTime(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 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{}}funcNewCacheV3(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)returnnil,false}return item.value,true}returnnil,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++returntrue})return size
}
代码测试:
funcTestCacheV3(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 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.funcNewItem(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 alivefunc(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 itemstype 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()returnlen(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()){deferfunc(){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 itemfunc(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()returnnil, 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()returnfalse}
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()}returntrue}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}returnnil, errors.New("cannot load item")}returnnil,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 sorttype 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{returnlen(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 []*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)}funcNewTable()*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(最近最少使用)算法,也支持自定义淘汰策略。
- 分布式支持:不支持。
版权归原作者 猫哥说 所有, 如有侵权,请联系我们删除。