0


【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库

本文主要介绍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算法来淘汰缓存项,具备一定的缓存性能。- 提供一致性哈希算法,可以解决节点扩容等问题。
  • 缺点:- 比较复杂,使用起来较为繁琐。- 只支持字符串类型的键值对。

本地缓存对比

参考文档:

在这里插入图片描述
下面对每个库的详细介绍:

  1. go-cache:
  • 描述:go-cache是一款简单而有效的内存缓存库,支持设置过期时间和GC机制。
  • 并发安全:是,使用Go的sync.Map实现数据的并发安全存储和访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:默认为LRU(最近最少使用)算法,也支持手动删除过期的缓存项。
  • 分布式支持:不支持。
  1. freecache:
  • 描述:freecache是一款高性能的内存缓存库,使用LRU算法进行缓存项的淘汰。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:固定大小,需要在初始化时指定总共可以缓存的字节数。
  • 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
  • 分布式支持:不支持。
  1. bigcache:
  • 描述:bigcache是一款高性能的内存缓存库,使用murmurhash哈希算法快速查找。
  • 并发安全:是,使用多个读写锁来实现高并发的访问控制。
  • 存储限制:固定大小,需要在初始化时指定最多可以缓存的条目数。
  • 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
  • 分布式支持:不支持。
  1. groupcache:
  • 描述:groupcache是一款支持分布式缓存的库,提供一致性哈希和HTTP请求缓存功能。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:支持自定义淘汰策略,例如手动删除过期的缓存项。
  • 分布式支持:是,支持分布式缓存,将数据分片存储在多个节点上,通过查询一致性哈希环来确定数据所在的节点。
  1. gocache:
  • 描述:gocache是一款快速、强大的内存缓存库,支持过期时间、并发安全和自定义淘汰策略。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:默认为LRU(最近最少使用)算法,也支持自定义淘汰策略。
  • 分布式支持:不支持。
标签: golang 缓存 开源

本文转载自: https://blog.csdn.net/baidu_33256174/article/details/134943507
版权归原作者 猫哥说 所有, 如有侵权,请联系我们删除。

“【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库”的评论:

还没有评论