美文网首页
go与cache

go与cache

作者: 安森老叔叔 | 来源:发表于2020-01-19 11:19 被阅读0次
        缓存是很重要的,个人暂且分为响应级别和键值级别等。实现的方式应该有很多,框架必然也有自带的,比如iris.Cache(响应级别的)。这里介绍的一个本地内存的键值级别缓存实现。
    
        工具:
    
    "github.com/goburrow/cache"
    
        先看源码:
    
    func NewLoadingCache(loader LoaderFunc, options ...Option) LoadingCache {
      c := newLocalCache()
      c.loader = loader  // 自定义取值方法
      for _, opt := range options {
        opt(c)
      }
      c.init()
      return c
    }
    
        该方法会返回一个配置了给定loader方法和设置的缓存对象。进入newLocalCache继续看。
    
    func newLocalCache() *localCache {
      return &localCache{
        cap: defaultCapacity,
        cache: cache{
          data: make(map[Key]*list.Element),
        },
        stats: &statsCounter{},
      }
    }
    
        可以看出,缓存的数据是保存在 localCache结构体的cache字段的cache结构体的data字段(有点绕),这个字段实质是一个map,值类型是链表。
    
        cap无非是设置缓存大小,stats是多个统计值的记录,用于根据业务优化缓存。
    
        由于按照实现,创建localCache对象后还需要执行init方法,我们再来看看init做了什么:
    
    func (c *localCache) init() {
      // LRU/Segmented LRU (default)/TinyLFU (experimental)三种可选,通过interface实现,对应不同的缓存操作
      c.entries = newPolicy(c.policyName)
      c.entries.init(&c.cache, c.cap)
      // 往缓存中添加键值
      c.addEntry = make(chan *entry, chanBufSize)
      // 访问缓存
      c.hitEntry = make(chan *list.Element, chanBufSize)
      // 删除缓存中键值时
      c.deleteEntry = make(chan *list.Element, chanBufSize)
      
      c.closeCh = make(chan struct{})
      go c.processEntries()
    }
    
        部分注释见上面代码。可见,这是对结构体的字段进行初始化。个人认为最为关键也最引人注目的是最后一行代码。一起看看:
    
    func (c *localCache) processEntries() {
      defer close(c.closeCh)
      for {
        select {
        // 关闭缓存时
        case <-c.closeCh:
          c.removeAll()
          return
        // 增加键时
        case en := <-c.addEntry:
          c.add(en)
          c.postWriteCleanup()
        // 访问缓存时
        case el := <-c.hitEntry:
          c.hit(el) // hit 命中缓存
          c.postReadCleanup()
        // 删除缓存中的键时
        case el := <-c.deleteEntry:
          if el == nil {
            c.removeAll()
          } else {
            c.remove(el)
          }
          c.postReadCleanup()
        }
      }
    }
    
        部分注释见上面代码。这是单独起一个协程,完成对结构体内的多个通道进行监控,并执行响应的逻辑。注意到c.postReadCleanup()和c.postReadCleanup()两个方法,分别执行对缓存键访问次数的统计和调用c.expireEntries()验证时效性,这是事件触发的。一起看看c.expireEntries():
    
    func (c *localCache) expireEntries() {
      if c.expireAfterAccess <= 0 {
        return
      }
      expire := currentTime().Add(-c.expireAfterAccess)
      remain := drainMax
      // ls是按accessed排序后的链表
      c.entries.walk(func(ls *list.List) {
        for ; remain > 0; remain-- {
          el := ls.Back()
          if el == nil {
            break
          }
          en := getEntry(el)
          if !en.accessed.Before(expire) {
            break
          }
          c.remove(el)
          c.stats.RecordEviction()
        }
      })
    }
    
        每当执行添加或访问操作时,机制都会更新entry结构体的accessed字段是一个time.Time类型,而结构体是上述链表中的值类型。
    
        通过从后一个个对比accessed字段和expire值进行时效性验证,默认逻辑如果近若干时间没访问,就会删除缓存值,当然下次访问就会更新。这个层面上可以根据业务需求定制,比如若service层对缓存的数据库记录进行更新,则Invalidate缓存中对应记录的键。
    

    如果键不存在于缓存中,机制将会调用开头讲的自定义取值方法执行逻辑,可以是从数据库中取值等等。为达目的,这个方法有特定的写法,那就是:

    func(k cache.Key) (cache.Value, error)
    

    整个过程对cache结构体的维护通过加锁实现准确,对缓存的命中数、非命中数、执行取值方法的加载时间、加载成功数、删除数都有详细记录,有助于基于此的优化。

        当然,缓存也可以直接简陋地保存在map中,业务中逻辑判断时效性,但功能和类型相对单一,对于代码维护和所谓“公共组件”来说,这也不是那么友好。
    

    其他实现方式也可以借助redis,还可以实现分布式缓存,天生队列和可设置expire,只是tcp相对程序内存而言,速度次之,但方案更普遍。纵观下来,这个工具应该是一个单机版的缓存。

        作者github给了示例,虽然没什么意义:
    
    package main
    ​
    import (
      "fmt"
      "math/rand"
      "time"
      "github.com/goburrow/cache"
    )
    ​
    func main() {
      // 定义按键取值的方式,这里只是返回键作为值。
      load := func(k cache.Key) (cache.Value, error) {
        return fmt.Sprintf("%d", k), nil
      }
      
      c := cache.NewLoadingCache(load,
        cache.WithMaximumSize(1000),
        cache.WithExpireAfterAccess(10*time.Second),
        cache.WithRefreshAfterWrite(60*time.Second),
      )
      // 返回一个每10毫秒生成值的通道
      getTicker := time.Tick(10 * time.Millisecond)
      // 返回一个每1秒生成值的通道  
      reportTicker := time.Tick(1 * time.Second)
      for {
        select {
        case <-getTicker:
          // 随机生成0-2000的键,访问缓存实例
          _, _ = c.Get(rand.Intn(2000))
        case <-reportTicker:
          st := cache.Stats{}
          c.Stats(&st)
          // 每1秒输出一次缓存实例的参数
          fmt.Printf("%+v\n", st)
        }
      }
    }
    
        输出如下,注意到第10秒出现了EvictionCount。
    
    image
        好了,今天就到这里。
    

    相关文章

      网友评论

          本文标题:go与cache

          本文链接:https://www.haomeiwen.com/subject/uvlrzctx.html