并发安全本地化存储go-cache读写锁实现多协程并发访问

 更新时间:2023年10月09日 10:57:05   作者:海生  
这篇文章主要介绍了并发安全本地化存储go-cache读写锁实现多协程并发访问,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

简介

go-cache广泛使用在go语言编程中,适合迎来在单机上 存储键值对形式的内存缓存。

在github上地址为 https://github.com/patrickmn/go-cache他在并发的时候,线程安全(读写锁) + map[string]interface{} + 过期时间 来作为go的本地化存储。

这也是他的三大特性:

  • 线程安全,通过读写锁支持多个协程并发访问
  • 不需要序列化,键值对形式,任意值类型map[string]interface{}
  • 自定义每个key的过期时间

数据结构

主要有Cache,以及其组成 cache,Item两个结构。

type Cache struct {
    *cache
    // If this is confusing, see the comment at the bottom of New()
    // 如果这令人困惑,请参阅New()底部的注释。
}
type cache struct {
    defaultExpiration time.Duration
    items             map[string]Item  //存储键值对
    mu                sync.RWMutex     // 读写锁,并发安全
    onEvicted         func(string, interface{})  // 被清除时的回调函数
    janitor           *janitor         // 脚本,定期清理过期数据
}
type Item struct {
    Object     interface{} // 存储的值
    Expiration int64   // 到期时间
}

创建Cache对象

使用New(defaultExpiration默认过期时间, cleanupInterval定时清理时间)函数来初始化。
传递到两个参数:key的过期时间,以及定时脚本清理过期数据的时间。

// Return a new cache with a given default expiration duration and cleanup interval.
// 返回具有给定默认过期和清除时间的 new cache新缓存
// If the expiration duration is less than one (or NoExpiration),
// the items in the cache never expire (by default), 
// 假如 到期时间为-1,永不过期
// and must be deleted manually.
// 并且只能手动删除。
// If the cleanup interval is less than one, expired items are not
// 如果清除时间小于1,过期item,在call之前是没有被删除的。
// deleted from the cache before calling c.DeleteExpired().
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
    items := make(map[string]Item)
    return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
    c := newCache(de, m)
    // This trick ensures that the janitor goroutine (which--granted it
    // 这个代码,确认是否启动 janitor看门人goroutine
    // was enabled--is running DeleteExpired on c forever) does not keep
    // the returned C object from being garbage collected. When it is
    // garbage collected, the finalizer stops the janitor goroutine, after
    // which c can be collected.
    C := &Cache{c}
    if ci > 0 {
       runJanitor(c, ci)  // 调用守卫进程来清理过期数据
       runtime.SetFinalizer(C, stopJanitor)
       // runtime.SetFinalizer(C, stopJanitor)会指定调用函数停止后台 goroutine,
       // 当 GC 准备释放对象时,会调用stopJanitor方法,
       // Run函数中j.stop通道会输出一个信号,从而退出协程。
    }
    return C
}
func runJanitor(c *cache, ci time.Duration) {
    j := &janitor{
       Interval: ci,
       stop:     make(chan bool),
    }
    c.janitor = j
    go j.Run(c)   // 调用守卫进程来清理过期数据
}
// 开启一个定时器,来定时清理数据。
func (j *janitor) Run(c *cache) {
    // 创建了一个计时器,时间到时ticker.C通道会输出一个值,调用DeleteExpired()函数
    // 该函数会通过遍历cache中的map[string]Item的过期时间,过期则直接从map中删除,
    // 如果该值有回调函数,则在删除后执行回调函数。
    ticker := time.NewTicker(j.Interval)
    for {
       select {
       case <-ticker.C:
          c.DeleteExpired()
       case <-j.stop:
          ticker.Stop()
          return
       }
    }
}
// Delete all expired items from the cache.
// 删除cache中所有的过期items
// 此时会加锁,如果定时清理的时间比较长,并且key比较多的话,
// 会导致一直被  清理协程锁住。其他的协程没法写入。
func (c *cache) DeleteExpired() {
    var evictedItems []keyAndValue
    now := time.Now().UnixNano()
    c.mu.Lock()
    // 过期删除的时候,需要上锁。
    for k, v := range c.items {
       // "Inlining" of expired
       if v.Expiration > 0 && now > v.Expiration {
          ov, evicted := c.delete(k)
          if evicted {
             evictedItems = append(evictedItems, keyAndValue{k, ov})
          }
       }
    }
    c.mu.Unlock()
    // 删除完解锁
    for _, v := range evictedItems {
       c.onEvicted(v.key, v.value)
    }
}
func (c *cache) delete(k string) (interface{}, bool) {
    if c.onEvicted != nil {
       if v, found := c.items[k]; found {
          delete(c.items, k)
          return v.Object, true
       }
    }
    delete(c.items, k)
    return nil, false
}

Get获取数据

// Get an item from the cache. Returns the item or nil, and a bool indicating
// whether the key was found.
// 从cache中Get一个item.返回item或者nil,和一个bool值。
func (c *cache) Get(k string) (interface{}, bool) {
    c.mu.RLock()
    // "Inlining" of get and Expired
    item, found := c.items[k]
    if !found {
       c.mu.RUnlock()
       return nil, false
    }
    // 获取到item,是否能返回还需要判断过期时间
    if item.Expiration > 0 {
       if time.Now().UnixNano() > item.Expiration {
          c.mu.RUnlock()
          return nil, false
       }
    }
    c.mu.RUnlock()
    return item.Object, true
}

Set保存数据

set保存数据,d的表达有三种情况:

  • 为0,使用默认的过期时间
  • 为-1,永不过期
  • 大于0的正常值,就是过期时间
// Add an item to the cache, replacing any existing item. If the duration is 0
// 将item添加到缓存中,以更换任何现有item。如果duration持续时间为0
// (DefaultExpiration), the cache's default expiration time is used. If it is -1
// (DefaultExpiration),使用缓存的默认到期时间。如果是-1
// (NoExpiration), the item never expires.
// (否开发),该项目永远不会到期。
func (c *cache) Set(k string, x interface{}, d time.Duration) {
    // "Inlining" of set
    var e int64
    if d == DefaultExpiration {
       d = c.defaultExpiration
    }
    if d > 0 {
       e = time.Now().Add(d).UnixNano()
    }
    c.mu.Lock()
    c.items[k] = Item{
       Object:     x,
       Expiration: e,
    }
    // TODO: Calls to mu.Unlock are currently not deferred because defer
    // adds ~200 ns (as of go1.)
    c.mu.Unlock()
}

常见问题

1、高并发

因为是加锁在整个cache上,相比那些加锁在分片上的其余缓存,并发会低一些。

2、关于内存溢出

如果设置的清理时间为0,就是永不清理,或者时间过长,有可能导致缓存越来越多。
因为没有主动清理,占用的缓存越闹越大。

3、关于定时清理

如果时间过长,一次清理太大,又因为加锁整个cache,可能会导致其他的协程无法写入。

4、关于map[string]interface{}存储的值,有可能会变。

interface{},如果存的是数组,或者指针等,当取出使用的时候,修改值,会导致缓存中的原始值变化。

以上就是patrickmn/go-cache源码阅读与分析的详细内容,更多关于patrickmn/go-cache源码阅读与分析的资料请关注脚本之家其它相关文章!

相关文章

  • golang sql连接池的实现方法详解

    golang sql连接池的实现方法详解

    database/sql是golang的标准库之一,它提供了一系列接口方法,用于访问关系数据库。下面这篇文章主要给大家介绍了关于golang sql连接池用法的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧
    2018-09-09
  • Go语言中声明变量(var)与常量(const)的完整指南

    Go语言中声明变量(var)与常量(const)的完整指南

    本文介绍了Go语言中变量和常量的定义方式,包括标准声明、类型推断、声明赋值、短变量声明、多变量声明等,以及常量的定义方式、导出规则、命名规则等,同时,还讲解了标识符的语法规则、作用域和遮蔽的概念
    2026-05-05
  • Go语言结构体(Struct)和接口(Interface)详解

    Go语言结构体(Struct)和接口(Interface)详解

    本文主要介绍了Go语言结构体(Struct)和接口(Interface),详解其定义、匿名字段、值/指针接收器等特性,探讨接口的隐式实现、类型断言及组合应用,下面就一起来了解一下
    2025-09-09
  • Go语言使用读写OPC详解

    Go语言使用读写OPC详解

    这篇文章主要介绍了Go语言使用读写OPC详解,图文讲解的很清晰,有感兴趣的同学可以学习下
    2021-03-03
  • Go语言命令行操作命令详细介绍

    Go语言命令行操作命令详细介绍

    这篇文章主要介绍了Go语言命令行操作命令详细介绍,本文重点介绍了go build、go clean、go fmt、go get等命令,需要的朋友可以参考下
    2014-10-10
  • Golang errors包快速上手

    Golang errors包快速上手

    errors 包是用于处理错误的标准库, errors 包提供的功能比较简单,使用起来非常方便,下面就来介绍一下,感兴趣的可以了解一下
    2025-05-05
  • Golang桥接模式讲解和代码示例

    Golang桥接模式讲解和代码示例

    桥接是一种结构型设计模式,可将业务逻辑或一个大类拆分为不同的层次结构,从而能独立地进行开发,本文将通过代码示例详细给大家介绍一下Golang桥接模式,需要的朋友可以参考下
    2023-06-06
  • Go定时器cron的使用详解

    Go定时器cron的使用详解

    本篇文章主要介绍了Go定时器cron的使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-01-01
  • Go语言运行环境安装详细教程

    Go语言运行环境安装详细教程

    这篇文章主要介绍了Go语言运行环境安装详细教程,本文讲解了Linux、MAC OS、Windows下的安装方法,同时讲解了验证是否安装成功、第三方工具安装运行环境的方法,需要的朋友可以参考下
    2014-10-10
  • golang pprof监控memory block mutex使用指南

    golang pprof监控memory block mutex使用指南

    这篇文章主要为大家介绍了golang pprof监控memory block mutex使用指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04

最新评论