缓存是很重要的,个人暂且分为响应级别和键值级别等。实现的方式应该有很多,框架必然也有自带的,比如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
好了,今天就到这里。
网友评论