groupcache源码走读(二):实现一个可以自动过时键的缓存

    上文提到的缓存模型,只能在访问键的时候才能判断这个键是否失效,是否应该删除。那若是一个系统须要大量使用本地缓存的话,可能会致使内存大量被无效键占用,而且GC应该也不能回收这部分空间。因此有的时候就须要有一个可以自动删除过时键的缓存策略。缓存

    通常地作法就是单独启动一个线程,用来轮训查看当前缓存中是否有过时的键。笔者在此处实现的方法也差很少,经过go自己的定时器来实现。spa

//主动过时策略的cache 防止cache太大占用内存过多
var My_Cache = &CacheSet{
        items: make(map[string]*CacheItem),
}

type CacheSet struct {
        sync.RWMutex
        items map[string]*CacheItem

        //下次缓存过时检查的间隔时间
        expireInterval time.Duration
        //Timer responsible for triggering cleanup
        //过时清理的定时器
        expireTimer *time.Timer
}

type CacheItem struct {
        sync.RWMutex
        Key string
        Val interface{}

        //缓存键的存活时间和建立时间
        liveDuration time.Duration
        ctime        time.Time
}

//add new key to cache set
func (set *CacheSet) Add(key string, data interface{}, expire time.Duration) *CacheItem {
        set.Lock()

        item := &CacheItem{
                Key:          key,
                Val:          data,
                liveDuration: expire,
                ctime:        time.Now(),
        }

        set.items[key] = item
        nextExpire := set.expireInterval
        set.Unlock()

        //知足如下条件须要当即开启缓存过时检测
        if expire > 0 && (nextExpire == 0 || expire < nextExpire) {
                set.expireCheck()
        }

        return item
}

func (set *CacheSet) Delete(key string) bool {
        set.Lock()
        defer set.Unlock()
        if _, ok := set.items[key]; ok {
                delete(set.items, key)
        }
}

func (set *CacheSet) Get(key string) interface{} {                                                                                                                                                                                                                            
        set.RLock()
        defer set.RUnlock()
        if val, ok := set.items[key]; ok {
                return val
        }

        return nil
}

    该缓存策略会在初次插入kv或者发现当前插入的键的过时时间比下次过时时间要短的时候主动开启缓存过时检测。过时检测代码以下:线程

//过时检测
func (set *CacheSet) expireCheck() {
        set.Lock()
        interval := set.expireInterval

        //当前有等待执行的定时器任务, 中止该任务并在后续启动新任务
        if set.expireTimer == nil {
                set.expireTimer.Stop()
        }

        now := time.Now()
        nextCheckTime := 0
        for k, v := range set.items {
                // item which will never be overdue
                if v.liveDuration == 0 {
                        continue
                }

                //判断键是否已通过期了,过时则删除,未过时的话则找出下一次最近的过时时间
                if now.Sub(v.ctime) >= v.liveDuration {
                        delete(set.items, k)
                } else {
                        if nextCheckTime == 0 || nextCheckTime > (v.liveDuration-now.Sub(v.ctime)) {
                                nextCheckTime = v.liveDuration - now.Sub(v.ctime)
                        }
                }
        }

        //若是缓存中还有kv存在,设置定时器按期evict过时键
        if nextCheckTime > 0 {
                set.expireTimer = time.AfterFunc(nextCheckTime, func() {
                        set.expireCheck()
                })
        }
        set.Unlock()
}

    该检测方法有两个特性:code

  • 经过expireTimer变量来实现的, 经过使用time.AfterFunc方法,定时器会在下一次有键过时的时候再次启动过时检测方法,而且是在子协程中执行,不会阻塞当前;
  • 而且只要当前系统中仍然还有缓存,该检测会一直持续下去。
相关文章
相关标签/搜索