golang-lru源码解析(2)2q缓存

自作多情 提交于 2020-01-28 02:24:29

2q

TwoQueueCache结构

TwoQueueCache 包含了三个基础的LRU缓存
simplelru的代码可戳 《golang-lru源码解析(1)项目结构、simplelru》
从名称上来看,分别用来存储:

  • 最近被使用的元素
  • 最频繁被使用的元素
  • 从最近缓存中被淘汰出来的元素

相比基本的LRU缓存,增加了一个recent缓存,recent用来保存新元素,而一个元素一旦被触发(新增或查询)两次及以上,那么机会被移动到frequent队列。将频繁使用的数据与新数据区分开来,当来到比较多的新数据的时候,不会挤占频繁但最近没有出现的数据,因为这些旧数据被命中的概率还是很大的。

type TwoQueueCache struct {
    size int // 整体缓存的最大元素数量
    recentSize int // recent缓存的最大元素数量
    
    recent simplelru.LRUCache
    frequent simplelru.LRUCache
    recentEvict simplelru.LRUCache
    // 锁,保证线程安全
    lock sync.RWMutex
}

const (
    // 默认的recent缓存占总容量的比例
    // 剩下的0.75为frequent的容量,recentEvict不占用总容量
    Default2QRecentRatio = 0.25
    // 默认的recentEvict的容量占总容量的比例
    // 这个容量是额外的,不占总容量的空间
    Default2QGhostEntries = 0.50
)

构造方法

默认情况下根据默认的容量比例,提供总容量size进行构造。也可以自己提供需要的缓存比例。

func New2Q(size int) (*TwoQueueCache, error) {
    return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries)
}
func New2QParams(size int, recentRatio float64, ghostRatio float64) (*TwoQueueCache, error) {
    if size <= 0 {
        return nil, fmt.Errorf("invalid size")
    }
    if recentRatio < 0.0 || recentRatio > 1.0 {
        return nil, fmt.Errorf("invalid recent ratio")
    }
    if ghostRatio < 0.0 || ghostRatio > 1.0 {
        return nil, fmt.Errorf("invalid ghost ratio")
    }
    recentSize := int(float64(size) * recentRatio)
    evictSize := int(float64(size) * ghostRatio)
    // 构造三个缓存
    recent, err := simplelru.NewLRU(size, nil)
    if err != nil {
        return nil, err
    }
    frequent, err := simplelru.NewLRU(size, nil)
    if err != nil {
        return nil, err
    }
    recentEvict, err := simplelru.NewLRU(evictSize, nil)
    if err != nil {
        return nil, err
    }
    // 初始化2q缓存
    c := &TwoQueueCache{
        size: size,
        recentSize: recentSize,
        recent: recent,
        frequent: frequent,
        recentEvict: recentEvict,
    }
    return c, nil
}

Get方法

// 从缓存中获取元素
func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) {
    // 加锁,线程安全
    c.lock.Lock()
    defer c.lock.Unlock()
    // 优先在frequent缓存中获取
    if val, ok := c.frequent.Get(key); ok {
        return val, ok
    }
    // 到recent中获取
    // 如果recent中存在,则移动到frequent中
    // 这里可以看到,recent中的元素只会出现一次,当多次出现时就被更新到frequent中了
    if val, ok := c.recent.Peek(key); ok {
        c.recent.Remove(key)
        c.frequent.Add(key, val)
        return val, ok
    }
    return nil, false
}

Add方法

添加元素的时候主要考虑两个逻辑
一是fre、rec、recE三个缓存判断的先后顺序
二是不同情况下对容量大小的判断,如果容量满了要进行淘汰

func (c *TwoQueueCache) Add(key, value interface{}) {
    // 加锁,线程安全
    c.lock.Lock()
    defer c.lock.Unlock()
    // 优先判断frequent中是否存在,如果存在则进行更新
    if c.frequent.Contains(key) {
        c.frequent.Add(key, value)
        return
    }
    // 其次判断recent是否存在,如果存在,移动到frequent
    if c.recent.Contains(key) {
        c.recent.Remove(key)
        c.frequent.Add(key, value)
        return
    }
    // 最后判断recentEvict是否存在,如果是,移动到frequent
    if c.recentEvict.Contains(key) {
        c.ensureSpace(true)
        c.recentEvict.Remove(key)
        c.frequent.Add(key, value)
        return
    }
    // 直接添加到recent
    c.ensureSpace(false)
    c.recent.Add(key, value)
    return
}
// ensureSpace 确定各个缓存队列的空间情况
func (c *TwoQueueCache) ensureSpace(recentEvict bool) {
    recentLen := c.recent.Len()
    freqLen := c.frequent.Len()
    // 如果总容量未达到预定大小,则什么也不用做
    if recentLen + freqLen < c.size {
        // 这里的size是整个2q缓存的大小,在内部包括recent和frequent两个缓存
        return
    }
    // 注意:代码走到这一步的情况是总的size已经达到预定大小。
    // recent缓存容量超标分两种情况
    // 1:元素已经存在缓存中,即recentEvict == true。此时进入if,需要recentLen > c.recentSize,即长度已经超标
    // 2:添加新元素,即recentEvict == false。此时recent的长度等于c.recentSize,加入新元素后就会超标
    // 从recent中对尾部元素进行移除,移除的元素加入到recentEvict中
    if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
        k, _, _ := c.recent.RemoveOldest()
        c.recentEvict.Add(k, nil)
        return
    }
    // 代码走到这一步的条件是缓存总量超标,但是recent没有超标。即frequent的容量超标
    c.frequent.RemoveOldest()
}

其他方法

func (c *TwoQueueCache) Len() int {
    c.lock.Rlock()
    defer c.lock.RUnLock()
    return c.recent.Len() + c.frequent.Len()
}
func (c *TwoQueueCache) Keys() []interface{} {
    c.lock.RLock()
    defer c.lock.RUnlock()
    k1 := c.frequent.Keys()
    k2 := c.recent.Keys()
    return append(k1, k2...)
}
func (c *TwoQueueCache) Remove(key interface{}) {
    c.lock.Lock()
    defer c.lock.Unlock()
    if c.frequent.Remove(key) {
        return
    }
    if c.recent.Remove(key) {
        return
    }
    if c.recentEvict.Remove(key) {
        return
    }
}
func (c *TwoQueueCache) Contains(key interface{}) bool {
    c.lock.RLock()
    defer c.lock.RUnlock()
    return c.frequent.Contains(key) || c.recent.Contains(key)
}
func (c *TwoQueueCache) Peek(key interface{}) (value interface{}, ok bool) {
    c.lock.RLock()
    defer c.lock.RUnlock()
    if val, ok := c.frequent.Peek(key); ok {
        return val, ok
    }
    return c.recent.Peek(key)
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!