开启掘金成长之旅!这是我参与「掘金日新计划 · 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出来。