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方法和结构体嵌套的方式来关闭掉定时器。具体而言:
本文转载自网络,原文链接:https://mp.weixin.qq.com/s/vqRfJtXcXg6fS_aeX0x8jQ...
前提条件 请您在购买前确保已完成注册和充值。详细操作请参见 如何注册公有云管...
很多企业普遍认为,迁移到云端会扩大攻击面,因此,在云端存储数据不如在本地存...
本文转载自微信公众号「程序员内点事」,作者程序员内点事。转载本文请联系程序...
2020年是充满动荡的一年,组织面临着众多挑战。进入2021年,大数据行业将会更加...
操作场景 当您的裸金属服务器需要跨POD通信时,可以为裸金属服务器添加一块增强...
目前 云服务器 有哪些?随着 云计算 的发展, 云服务器 市场上提供 云服务器 的...
一、定义函数 在Python中,定义一个函数要使用def语句,依次写出函数名、括号、...
作者 | 刘晓敏 来源 | 阿里巴巴云原生公众号 Seata 是一款简单易用,高性能、开...
本月DataWorks产品月刊为您带来 产品活动 1.参与阿里云DataWorks问卷调研 (Aliyu...