美文网首页
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
    好了,今天就到这里。

相关文章

  • Golang 开源项目cache2go 解读

    参考启航 - cache2go源码分析cache2go - cachetable源码分析cache2go源码最后一...

  • go与cache

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

  • go的内存cache库

    cache2go

  • 侧面剖析cache2go

    学习开源缓存库,cache2g0是作为Go新手来说,比较容易上手的library。 Cache2go Concur...

  • Go Cache

    缓存 缓存最简单的莫过于存储在内存中的键值对,键值对在Golang中称之为map。使用map做内存缓存时,每次有新...

  • twrp提示 E1001: Failed to update s

    in TWRP go to advanced > terminal, then create /cache/rec...

  • 无标题文章

    go test -race -bench Benchmark_local_cache -run =^$ -cpu ...

  • Flutter web 跨域问题

    1- Go to flutter\bin\cache and remove a file named: flutt...

  • Go每日精选(2019-07-07)

    1. GO Timer 机制探究 2.理解 Go context 3.Go 程序是怎样跑起来的 4.cache2g...

  • 什么是 cpu cache

    最近阅读 nginx, go 代码时经常看到结构体 cache line 对齐,比如 go timer 全局数组。...

网友评论

      本文标题:go与cache

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