• Go 每日一库之 gocache 缓存


    什么是 go-cache

    go-cache 是一个轻量级的基于内存的 K-V 储存组件,内部实现了一个线程安全的 map[string]interface{},适用于单机应用。具备如下功能:

    • 线程安全,多 goroutine 并发安全访问;
    • 每个 item 可以设置过期时间(或无过期时间);
    • 自动定期清理过期的 item;
    • 可以自定义清理回调函数;

    这里的 item 指的是 map 里的元素。

    go-cache 一般用作临时数据缓存来使用,而不是持久性的数据存储。对于某些停机后快速恢复的场景,go-cache支持将缓存数据保存到文件,恢复时从文件中将数据加载到内存。

    先来“浅尝”下,好不好用试试就知道。

    如何使用

    func main() {
     c := cache.New(10*time.Second, 30*time.Second) // 默认过期时间10s;清理间隔30s,即每30s钟会自动清理过期的键值对

     // 设置一个键值对,过期时间是 3s
     c.Set("a", "testa", 3*time.Second)

     // 设置一个键值对,采用 New() 时的默认过期时间,即 10s
     c.Set("foo", "bar", cache.DefaultExpiration)

     // 设置一个键值对,没有过期时间,不会自动过期,需要手动调用 Delete() 才能删除
     c.Set("baz", 42, cache.NoExpiration)

     v, found := c.Get("a")
     fmt.Println(v, found) // testa,true

     <-time.After(5 * time.Second) // 延时5s

     v, found = c.Get("a") // nil,false
     fmt.Println(v, found)

     <-time.After(6 * time.Second)
     v, found = c.Get("foo") // nil,false
     fmt.Println(v, found)

     v, found = c.Get("baz") // 42,true
     fmt.Println(v, found)

     // 完整例子请关注公众号【Golang来啦】,后台发送关键字 gocache 获取
     //TestCache()
     //TestCacheTimes()
     //TestNewFrom()
     //TestOnEvicted()
     //TestFileSerialization()
    }

    下面我们来看下 go-cache 内部是如何实现前面说的那些功能的,另外阅读完源码我们还能掌握一种优雅地关闭后台 goroutine 的方法。

    常量与结构体

    常量

    const (
     
     NoExpiration time.Duration = -1    // 无有效时间

     DefaultExpiration time.Duration = 0   // 表示采用默认时间
    )

    这两个参数可以用作 New() 函数的第一个入参,则默认过期时间小于0,意味着添加键值对时如果采用默认过期时间,则该键值对不会过期,因为 DeleteExpired() 方法会判断 v.Expiration 是否大于 0,大于 0 时才会自动删除。如果想删除需要手动 Delete() 方法。

    添加键值对,比如执行 Set()、Add() 等操作时,这两个常量也可以作为参数,NoExpiration 表示没有过期时间,DefaultExpiration 表示采用默认的过期时间。

    结构体

    主要的结构体包括下面这些:


    type Item struct {  // 键值对
     Object     interface{}     // 存放 K-V 的值,可以存放任何类型的值
     Expiration int64   // 键值对的过期时间(绝对时间)
    }

    type Cache struct {   // 对外使用的 Cache
     *cache  // cache 实例
    }

    type cache struct {
     defaultExpiration time.Duration   // 默认的过期时间,添加一个键值对时如果设置默认的过期时间(即代码里的 DefaultExpiration)则会使用到该值
     items             map[string]Item   // 存放的键值对
     mu                sync.RWMutex   // 读写锁
     onEvicted         func(string, interface{})  // 删除key时的回调函数
     janitor           *janitor  // 定期清理器 定期检查过期的 Item
    }

    type janitor struct { // 清理器结构体
     Interval time.Duration // 清理时间间隔
     stop     chan bool     // 是否停止
    }


    主要流程和功能

    实例化 Cache


    // 参数:默认过期时间、清理时间间隔
    func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
     items := make(map[string]Item)   // 初始化 items
     return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)   // 根据需要是否创建清理器
    }

    func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
     c := newCache(de, m)   // 实例化cache
     C := &Cache{c}
     if ci > 0 { // 清理间隔大于 0 时才会开启自动清除过期item的功能,否则需要手动调用删除方法-DeleteExpired() 删除过期item
      runJanitor(c, ci)   // 开启清理器
      runtime.SetFinalizer(C, stopJanitor)   // 优雅地停止 goroutine 的一种方式,我们放在后面讲
     }
     return C
    }

    // 如果 de 小于等于0,则默认过期时间小于0,意味着添加键值对时如果采用默认过期时间,则该键值对不会过期
    // 因为 DeleteExpired() 方法会判断 v.Expiration 是否大于 0,大于 0 时才会自动删除。 如果想删除需要手动 Delete() 方法。
    func newCache(de time.Duration, m map[string]Item) *cache {
     if de == 0 {
      de = -1
     }
     c := &cache{
      defaultExpiration: de,
      items:             m,
     }
     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 := time.NewTicker(j.Interval)  // 开定时器
     for {
      select {
      case <-ticker.C:
       c.DeleteExpired()   // 定时调用 DeleteExpired() 执行过期删除操作
      case <-j.stop:     // 接收到停止清理器的信号,下面便停止定时器并返回,退出协程
       ticker.Stop()
       return
      }
     }
    }

    添加操作

    添加类操作有四个函数可以使用,分别是:

    Set()

    func (c *cache) Set(k string, x interface{}, d time.Duration) {
     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, 
     }
     c.mu.Unlock()
    }

    函数的逻辑还是比较简单的,需要注意的是过期时间是绝对时间。

    SetDefault()

    func (c *cache) SetDefault(k string, x interface{}) {
     c.Set(k, x, DefaultExpiration)
    }

    SetDefault() 内部调用了 Set() 函数实现,使用默认过期时间。

    Add()如果待添加的 K 不存在的话才能添加成功,包括已过期但还未被删除,否则返回错误。

    Replace()

    如果待添加的 K 已存在的话才能执行成功,否则返回错误。操作条件真好与 Add() 相反。

    上面两个函数逻辑比较简单就不放源码,大家自行查看。

    删除

    删除操作主要有两个,执行删除操作的时候都会判断是否需要执行删除回调函数,下面我们可以看到。

    Delete() 常规删除,不管是否过期都会删除。

    DeleteExpired() 用于执行批量删除操作,只会删除已过期的键值对。

    func (c *cache) Delete(k string) {
     c.mu.Lock()
     v, evicted := c.delete(k) // v 是 k 对应的值
     c.mu.Unlock()
     if evicted {   // 是否需要执行删除回调函数
      c.onEvicted(k, v)
     }
    }

    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
      }
     }
     // 能执行到这里说明有两种情况:
     // 1.没有设置删除回调函数 2.设置了删除回调函数,但是键值对不存在
     delete(c.items, k)
     return nil, false
    }

    // 将所有过期的键值对从缓存中删除,如果设置有回调函数,则会将键值对作为参数调用回调函数
    func (c *cache) DeleteExpired() {
     var evictedItems []keyAndValue  // 临时存放已经过期的键值对
     now := time.Now().UnixNano()
     c.mu.Lock()
     for k, v := range c.items {
      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)  // 执行删除回调函数
     }
    }

    关于删除回调函数的详细用法,请关注公众号【Golang来啦】,后台发送关键字 gocache 获取。

    Golang来啦
    欢迎来到Gopher自留地! 专注于分享Golang知识、职场心得和生活感悟。带你入门,完成进阶,顺利掌握Golang ! Golang已来,上车!!
    205篇原创内容

    备份恢复数据

    虽然 go-cache 比较倾向于当做缓存数据来使用,但还是提供了备份数据和恢复数据的操作,数据使用 gob 序列化。

    详细用法请关注公众号【Golang来啦】,后台发送关键字 gocache 获取。

    其他一些操作

    其他一些操作包括:

    • ItemCount(),返回所有数据的条数,这里的条数包括已过期但还未被删除的数量;
    • Flush(),清空数据;
    • Items(),返回数据的未过期的数据,可以使用 NewFrom() 恢复数据;

    从各种操作的源码就可以看出,go-cache 内部通过使用读写锁(sync.RWMutex)保证并发安全。

    优雅地关闭 goroutine

    这个问题之前有提到过,也是一种优雅停止 goroutine 的一种方式。我们把相关的代码拉出来看下:

    func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
     c := newCache(de, m)   // 实例化cache
     C := &Cache{c}
     if ci > 0 {
      runJanitor(c, ci)   // 开启清理器
      runtime.SetFinalizer(C, stopJanitor)   // 指定调用函数停止后台 goroutine
     }
     return C
    }

    func stopJanitor(c *Cache) {
     c.janitor.stop <- true
    }

    type janitor struct { // 清理器结构体
     Interval time.Duration // 清理时间间隔
     stop     chan bool     // 是否停止
    }

    func (j *janitor) Run(c *cache) {
     ticker := time.NewTicker(j.Interval)  // 开定时器
     for {
      select {
      case <-ticker.C:
       c.DeleteExpired()   // 定时调用 DeleteExpired() 执行过期删除操作
      case <-j.stop:     // 接收到停止清理器的信号,下面便停止定时器并返回,退出协程
       ticker.Stop()
       return
      }
     }
    }

    // 运行清理器
    func runJanitor(c *cache, ci time.Duration) {
     j := &janitor{    // 实例化清理器
      Interval: ci,
      stop:     make(chan bool),
     }
     c.janitor = j
     go j.Run(c)    // 开协程,定时清理器跑起来!!
    }

    这里退出后台 goroutine 使用的是下面这个函数,当 GC 准备释放对象时,会调用该函数指定的方法。

    runtime.SetFinalizer(obj,func(obj *typeObj))

    当我们取消对 C 对象的引用时,如果不退出 runJanitor() 开启的 goroutine 就会造成内存泄漏。当 gc 准备释放 C 时,会调用指定函数 stopJanitor(),Run() 方法便能收到信号,退出协程,gc 也会将 c 释放掉。

    总结

    go-cache 的源码结构清晰,代码量也不多,还能掌握一种优雅退出 goroutine 的防止,推荐阅读,有问题可以发信息给我,相互学习进步!

    项目地址:https://github.com/patrickmn/go-cache

  • 相关阅读:
    linux删除/var/log/下面所有日志 如何重新记录日志
    DIV里的内容自动换行
    it冲突:commit your changes or stash them before you can merge. 解决办法
    git切换到远程分支
    【异常】warning: refname 'feature1.3.0' is ambiguous.导致git merge失败
    在此篇文章中,我们将用 15 分钟对 PHP v7.x 版本更改进行简要回顾
    input元素所有type类型及相关作用
    微信自动关闭内置浏览器页面,返回公众号窗口 WeixinJSBridge.call('closeWindow')
    css背景渐变色
    数组的forEach和map和for方法的区别
  • 原文地址:https://www.cnblogs.com/cheyunhua/p/16802281.html
Copyright © 2020-2023  润新知