go-cache库源码解析

453 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

简介

go-cache库是适用在单机应用程序上的一个键值对形式的内存缓存库,它本质上由一个线程安全的map[string]interface{}和一个过期时间组成,不需要经过序列化就能在网络上传输其内容。 任何对象都可以在给定的持续时间内或永久地存储,并且缓存可以被多个goroutine安全地使用。 它有如下功能:

  • 线程安全,支持多个协程并发访问
  • 支持对每个key设置过期时间,类似redis
  • 支持设置自动清理过期key的时间间隔
  • 可以自定义清理key时执行的回调函数

基础结构

type Item struct {
   Object     interface{}
   Expiration int64
}

type Cache struct {
   *cache
   // If this is confusing, see the comment at the bottom of New()
}

type cache struct {
   defaultExpiration time.Duration              // 过期时间
   items             map[string]Item            // 存储map
   mu                sync.RWMutex               // 读写锁,用于保证线程安全
   onEvicted         func(string, interface{})  // 值被删除时执行的回调函数
   janitor           *janitor                   // 定时清理器(清理key)
}

cache中所有的值都存储在cache结构中的items里,它是一个map,每一个key都由一个接口类型和过期时间组成(Item)

创建Cache对象

首先使用New函数创建一个Cache对象,传递两个参数:de(key过期时间)和ci(清理key的时间间隔)。 在New()中,会构造一个空的map[string]Item对象赋值给cache,同时,如果传递的 清理key的时间间隔 这个参数大于0,就会继续创建一个定时清理器Janitor对象,它会创建一个协程去管理何时清理过期的key,清理过程如下:

func (j *janitor) Run(c *cache) {
   ticker := time.NewTicker(j.Interval) // 计时器
   for {
      select {
      case <-ticker.C:      // 清理
         c.DeleteExpired()
      case <-j.stop:        // 停止清理器
         ticker.Stop()
         return
      }
   }
}

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)    // 执行回调
   }
}

Run函数创建了一个计时器,时间到时ticker.C通道会输出一个值,所以会调用DeleteExpired()函数,该函数会通过遍历cache中的map[string]Item的过期时间,过期则直接从map中删除,如果该值有回调函数,则在删除后执行回调函数。

runtime.SetFinalizer(C, stopJanitor)会指定调用函数停止后台 goroutine,当 GC 准备释放对象时,会调用stopJanitor方法,Run函数中j.stop通道会输出一个信号,从而退出协程。

存取过程

Set()

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()
}

函数的逻辑还是比较简单的,需要注意的是Set是覆盖写入,并且过期时间是绝对时间。

Get()

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
   }
   if item.Expiration > 0 {
      if time.Now().UnixNano() > item.Expiration {
         c.mu.RUnlock()
         return nil, false
      }
   }
   c.mu.RUnlock()
   return item.Object, true
}

如果该key在map中存在,并且过期时间没到,才能Get出来。