沉浸式go-cache源码阅读!
后台-插件-广告管理-内容页头部广告(手机) |
大家好,我是豆小匠。
这期来阅读go-cache的源码,了解本地缓存的实现方式,同时掌握一些阅读源码的技巧~
1. 源码获取
git clone https://github.com/patrickmn/go-cache.git- 1
用Goland打开可以看到真正实现功能的也就两个go文件,cache.go 1162行,sharded.go 193行,共1355行,用来作为源码阅读的练手素材是非常合适的。
通过README.md文件,可以了解这个包的使用方法:
import ( "fmt" "github.com/patrickmn/go-cache" "time" ) func main() { // 创建一个缓存对象,默认过期时间5分钟,每10分钟清理一次缓存 c := cache.New(5*time.Minute, 10*time.Minute) // 设置缓存key:foo,value:bar,过期时间是包里定义的一个常量,一会看看具体定义了啥 c.Set("foo", "bar", cache.DefaultExpiration) // 获取key为foo的缓存,通过类型断言获取原始的数据 foo, found := c.Get("foo") if found { MyFunction(foo.(string)) } }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
2. 源码阅读
上面我们看到,创建一个缓存实例,需要传入缓存清理的间隔,也就是说缓存的删除不是根据缓存过期时间实时删除的,那怎么处理才能让已过期的缓存在逻辑上失效呢?
带着疑问,开始阅读cache.go文件。
2.1. Cache定义
type Cache struct { *cache // 为何套娃,先按下不表 } type cache struct { defaultExpiration time.Duration // 默认过期时间 items map[string]Item // 所有缓存key value,用一个map保存,key是string,value是一个结构体Item mu sync.RWMutex // 读写锁,可以知道go-cache大概率是并发安全的 onEvicted func(string, interface{}) // 这啥,先不管 janitor *janitor // 这啥,先不管 } type Item struct { Object interface{} // 真正存储的缓存数据 Expiration int64 // 这个数据的过期时间 }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
看完Cache结构体的定义,先有个整体印象,再看它的方法实现~
2.2. Cache初始化
在README.go,我们已经知道,初始化的函数是New(defaultExpiration, cleanupInterval time.Duration),双击shift,输入New,就能找到这个函数。
type janitor struct { Interval time.Duration // 清理过期缓存的间隔 stop chan bool // 接受停止协程的信号 } func New(defaultExpiration, cleanupInterval time.Duration) *Cache { items := make(map[string]Item) // 定义缓存容器,会存到cache对象的items return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) // 创建一个带有清理协程的Cache对象 } func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { c := newCache(de, m) // 生成小写那个cache对象(私有) C := &Cache{c} if ci > 0 { // 传入定时删除缓存时间大于0,启动看清理协程 runJanitor(c, ci) // 启动清理协程,定时删除过期的cache key runtime.SetFinalizer(C, stopJanitor) // 设置C被回收时,执行函数停止清理协程 } return C }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
runtime.SetFinalizer:对象可以关联一个SetFinalizer函数, 当gc检测到unreachable对象有关联的SetFinalizer函数时,会执行关联的SetFinalizer函数, 同时取消关联。 这样当下一次gc的时候,对象重新处于unreachable状态并且没有SetFinalizer关联, 就会被回收。
通过上面源码的阅读,我们可以知道:
- 清理过期缓存通过一个清理协程定期清理。
- 当Cache不可达时,GC会触发停止janitor协程的函数,下一次GC,Cache和cache(内部cache对象)都会被回收。(如果janitor协程和Cache绑定,Cache对象不会被回收,有内存泄露的风险)
- 1
- 2
如果清理协程绑定在Cache对象,因为协程一直在运行,即使在使用者看来c已经设置为nil,cache不再使用,GC也无法回收Cache。
2.3. 缓存失效判断
Cache上是不挂方法的,方法都挂在内部对象cache上。
我们先看Get方法:
func (c *cache) Get(k string) (interface{}, bool) { c.mu.RLock() // 加读锁 item, found := c.items[k] if !found { c.mu.RUnlock() return nil, false } // 下面这里会判断item里的过期时间,过期时间小于当前时间,则在逻辑上失效,返回nil, false if item.Expiration > 0 { // 如果expiration为0,说明设置的是永不过期 if time.Now().UnixNano() > item.Expiration { c.mu.RUnlock() return nil, false } } c.mu.RUnlock() return item.Object, true }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
看源码可以很清晰的看到,缓存过期不是通过是否存在key来判断的,而是通过item里存的expiration时间来判断,因此定时清理缓存是为了清理空间。
2.4. 总体梳理
其他方法都非常明确,我们可以挑几个常用的看看实现,最后整理下cache这个类的成员变量和方法,画个图,完事!
前面埋的坑:onEvicted 是删除key的回调函数。
另外sharded.go文件是一个实验性的代码,用于缓存分片,目前还没对外暴露。
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。
在线投稿:投稿 站长QQ:1888636
后台-插件-广告管理-内容页尾部广告(手机) |