前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 ><源码阅读>Go Cache

<源码阅读>Go Cache

原创
作者头像
Porco1Rosso
修改2021-05-17 17:28:25
2K1
修改2021-05-17 17:28:25
举报

源码阅读是2020年开始的一个长期计划,主要目的有两个:1.提高自己对GO语言的理解,2.理解功能设计的原理。关于第二点,说的详细点就是,不仅要了解怎么做的,还要知道为什么这么做,有哪些好处,在什么场景下适用。最终提高自己对代码的敏感度,丰富自己的工具箱,让自己面对业务问题时能够从容不迫。

项目地址 https://github.com/patrickmn/go-cache

学习总结

  • Go Cache 算是比较常用的本地缓存工具。他结构清晰,操作简单,非常实用。目前最常用的场景是大流量下为了避免redis出现大key热key的问题采用的本地缓存。
  • runtime.SetFinalizer方法的特点和使用场景。

结构体设计

代码语言:txt
复制
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 是并发安全。这一块它提供的方法还是比较全面的,我们只看一些常用的方法。

代码语言:txt
复制
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()
}

淘汰过期数据

这一块,我们不仅看她如何做淘汰过期数据,最主要看下它是如何跑起来的。

初始化
代码语言:txt
复制
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在一定程度上就扮演着“退出”的角色

淘汰过期缓存
代码语言:txt
复制
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 - 知乎

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 学习总结
  • 结构体设计
    • 淘汰过期数据
      • 初始化
      • 淘汰过期缓存
    • 数据持久化
    • LRU算法的实现
    • 扩展:GoCache 是怎么关闭掉的
    相关产品与服务
    文件存储
    文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档