源码阅读是2020年开始的一个长期计划,主要目的有两个:1.提高自己对GO语言的理解,2.理解功能设计的原理。关于第二点,说的详细点就是,不仅要了解怎么做的,还要知道为什么这么做,有哪些好处,在什么场景下适用。最终提高自己对代码的敏感度,丰富自己的工具箱,让自己面对业务问题时能够从容不迫。
项目地址 https://github.com/patrickmn/go-cache
type Item struct {
//缓存结构的基本单位 Item,包含两个字段
Object interface{} //值字段
Expiration int64 //过期时间,实际值为设置时的毫秒时间戳 + 过期时间。
}
//Item 唯一的方法。判断当前Item是否过期。
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}
type Cache struct {
*cache //这里非常重要,再原有结构的基础上在包一层的目的,便于做垃圾回收。
}
type cache struct {
defaultExpiration time.Duration //默认的过期时间
items map[string]Item //数据存储模块
mu sync.RWMutex //用来实现并发安全的锁
onEvicted func(string, interface{}) //可以自行设置的删除后置函数
janitor *janitor //定时器
}
这一块有一些比较好的经验可以学习:
设置缓存这一块代码也非常简单,主要是使用 sync.RWMutex来控制并发。因此,我们说go cache 是并发安全。这一块它提供的方法还是比较全面的,我们只看一些常用的方法。
func (c *cache) Set(k string, x interface{}, d time.Duration) {
var e int64
// 如果 d 为0 则取一开始设置的默认值。如果为-1,那么久永不过期
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,
}//赋值
c.mu.Unlock()//解锁,源码在这里有句注释:TODO: Calls to mu.Unlock are currently not deferred because defer adds ~200 ns (as of go1.)
}
//这个方法与SET 唯一的区别 不加锁。
func (c *cache) set(k string, x interface{}, d time.Duration) {
...
}
//中间还有一些Add和Replace的方法。Add可以保证一定是新增一个KEY。Replace可以保证一定存在这个KEY,并且更新它。
...
//读取一个缓存,也是并发安全的
func (c *cache) Get(k string) (interface{}, bool) {
c.mu.RLock() //先加一个读锁。
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
}
//删除缓存,并执行回调
func (c *cache) Delete(k string) {
c.mu.Lock() //加锁
v, evicted := c.delete(k)//执行删除
c.mu.Unlock() //解锁
if evicted { //执行删除后置操作。这里是先删除了缓存,再执行删除回调方法。
c.onEvicted(k, v)
}
}
//删除操作 真正的核心方法,只删除不负责执行回调。
func (c *cache) delete(k string) (interface{}, bool) {
if c.onEvicted != nil { //如果有删除后的回调方法,就返回true
if v, found := c.items[k]; found {
delete(c.items, k)//map内置的删除方法
return v.Object, true
}
}
delete(c.items, k)
return nil, false
}
//全量复制方法。这里是开辟了一块新的内存,将现有的缓存内容全部读出来,原有的缓存不受影响。这里加的也是读锁。
func (c *cache) Items() map[string]Item {
c.mu.RLock()
defer c.mu.RUnlock()
m := make(map[string]Item, len(c.items))
now := time.Now().UnixNano()
for k, v := range c.items {
// "Inlining" of Expired
if v.Expiration > 0 {
if now > v.Expiration {
continue
}
}
m[k] = v
}
return m
}
//重置缓存,并不逐个删除,而是直接设置为空!
func (c *cache) Flush() {
c.mu.Lock()
c.items = map[string]Item{}
c.mu.Unlock()
}
这一块,我们不仅看她如何做淘汰过期数据,最主要看下它是如何跑起来的。
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
//构建基础结构
c := newCache(de, m)
C := &Cache{c}
if ci > 0 {
//存在清理周期的话,开启清理定时器。
runJanitor(c, ci)
runtime.SetFinalizer(C, stopJanitor)//将缓存结构和关闭定时器的方法绑定。第一次GC扫到C的时候,执行方法并解绑。实现安全关闭定时器。
}
return C
}
//返回一个Cache的实体。入参是一个默认过期时间,一个清理过期缓存的周期。
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
runtime.SetFinalizer 我们放在最后详细说。我们可以看到整个源码里是没有close方法的,实际上runtime.SetFinalizer在一定程度上就扮演着“退出”的角色
func runJanitor(c *cache, ci time.Duration) {
j := &janitor{
Interval: ci,
stop: make(chan bool),//停止信号
}
c.janitor = j //设置cache 的定时器
go j.Run(c)
}
//定时器运行方法
func (j *janitor) Run(c *cache) {
ticker := time.NewTicker(j.Interval)
for {
//此时goruntine 会阻塞在这里,等着接受信号
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 {
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) //逐个执行回调方法。
}
}
这一块整体设计的非常的精炼,并且也实现了基本的数据持久化的功能,这是本身不会定时做持久化,需要手动调用接口来实现。尤其需要注意的是,这几个方法也是并发安全的,换句话说,是会加锁的。无论是读锁还是写锁,他都要全程加锁,这个开销是需要慎重考虑的。
另外,这一块的代码涉到了序列化与反序列化的功能,主要是gob包。具体的用法和案例可以看:
gob - The Go Programming Language
Golang Gob编码(gob包的使用)_cqu_jiangzhou的博客-CSDN博客
整个GoCache本身是没有实现LRU算法的,他的淘汰机制就是定时器来看过期时间。这里可以看下LRU算法的讲解,并且附带着GO代码:缓存淘汰算法—LRU算法 - 知乎
另外,go-zero中的cache 中实现了LRU算法,可以看下的源码。zero-doc/collection.md at main · tal-tech/zero-doc · GitHub
举个场景,如果cache已经没用了,可以被GC了,这个时候因为有后台线程存在,这个cache会一直存在,不会被GC回收掉。正常情况下,我们需要声明一个显式的CLOSE方法,当我们关闭一个cache的时候,把后台的定时器关闭掉。这样子就可以正常被GC了。
GoCache没有用这个策略,使用了runtime.SetFinalizer方法和结构体嵌套的方式来关闭掉定时器。具体而言:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。