当前位置:主页 > 查看内容

<源码阅读>Go Cache

发布时间:2021-04-30 00:00| 位朋友查看

简介:结构体设计 type Item struct { //缓存结构的基本单位 Item,包含两个字段Object interface{} //值字段Expiration int64 //过期时间,实际值为设置时的毫秒时间戳 + 过期时间。}//Item 唯一的方法。判断当前Item是否过期。func (item Item) Expired() bool {……

结构体设计

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  //定时器
}

这一块有一些比较好的经验可以学习:

  1. 结构简单,职责明确。结构简单,维护成本就很好把控,职责明确了,可以确保操作的结果也是明确。
  2. 合理的封装和嵌套。我们拿到的最终的结构体Cache 是三层封装结构体。第一层是为了便于做GC;第二层是缓存的基本属性,包括删除回调,并发锁,定时器等。第三层,就是我们要操作的缓存的实体。基本功能整体来看,一共是实现了三个方面的内容:
    1. 设置缓存
    2. 淘汰过期数据
    3. 数据持久化

设置缓存这一块代码也非常简单,主要是使用 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博客

LRU算法的实现

整个GoCache本身是没有实现LRU算法的,他的淘汰机制就是定时器来看过期时间。这里可以看下LRU算法的讲解,并且附带着GO代码:缓存淘汰算法—LRU算法 - 知乎

另外,go-zero中的cache 中实现了LRU算法,可以看下的源码。zero-doc/collection.md at main · tal-tech/zero-doc · GitHub

扩展:GoCache 是怎么关闭掉的

举个场景,如果cache已经没用了,可以被GC了,这个时候因为有后台线程存在,这个cache会一直存在,不会被GC回收掉。正常情况下,我们需要声明一个显式的CLOSE方法,当我们关闭一个cache的时候,把后台的定时器关闭掉。这样子就可以正常被GC了。

GoCache没有用这个策略,使用了runtime.SetFinalizer方法和结构体嵌套的方式来关闭掉定时器。具体而言:

  1. 声明一个壳Cache,实际的结构体cache是壳的匿名字段。
  2. 使用runtime.SetFinalizer方法把cache里的关闭定时器方法和壳绑定。
  3. 当GC第一次发现壳已经不再存活可以被回收了,就先执行runtime.SetFinalizer绑定的方法,关闭定时器。解除绑定。此时壳和cache本身就全部处在可回收状态了。
  4. GC下次运行时会回收掉壳以及壳里的cache使用runtime.SetFinalizer优雅关闭后台goroutine - 知乎

本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐