golang-lru Cache (二)2Q

LRU的问题

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

2Q Cache

2Q算法

2Q算法有两个缓存队列,一个是FIFO队列,用于保存最近访问的数据;一个是LRU队列,用于保存热点数据。

golang-lru实现

在golang-lru中,两个队列都是用simplelru实现。

type TwoQueueCache struct {
    size       int
    recentSize int

    recent      simplelru.LRUCache
    frequent    simplelru.LRUCache
    recentEvict simplelru.LRUCache
    lock        sync.RWMutex
}

其中recent 用于保存最近访问的数据,frequent用于保存热点数据,recentEvict保存从最近队列中剔除的数据。

// Add adds a value to the cache.
func (c *TwoQueueCache) Add(key, value interface{}) {
    c.lock.Lock()
    defer c.lock.Unlock()

    // Check if the value is frequently used already,
    // and just update the value
    if c.frequent.Contains(key) {
        c.frequent.Add(key, value)
        return
    }

    // Check if the value is recently used, and promote
    // the value into the frequent list
    if c.recent.Contains(key) {
        c.recent.Remove(key)
        c.frequent.Add(key, value)
        return
    }

    // If the value was recently evicted, add it to the
    // frequently used list
    if c.recentEvict.Contains(key) {
        c.ensureSpace(true)
        c.recentEvict.Remove(key)
        c.frequent.Add(key, value)
        return
    }

    // Add to the recently seen list
    c.ensureSpace(false)
    c.recent.Add(key, value)
    return
}

当增加一个新记录时:

  • 如果frequent队列中存在该数据则更新,否则下一步;
  • 如果recent队列存在该数据,则将其移至frequent队列,否则下一步;
  • 如果recentEvict队列存在该数据,则确保队列有空间后将其移至frequent,否则下一步;
  • 确保队列有空闲,并将其添加至recent

第一步,frequent 长度不变,整个2q Cache长度不变;第二步recent长度减一,frequent长度加一,总长度不变;第三步,recentEvict长度减一,frequent要增加新数据,由于recentEvict不计算在cache size内,总长度加一,所以需要确认队列是否有空闲,如果没有则remove oldest;第四步,recent长度加1,总长度加一,需要判断队列是否有空闲。

判断队列是否有空闲的逻辑比较简单:

func (c *TwoQueueCache) ensureSpace(recentEvict bool) {
    // If we have space, nothing to do
    recentLen := c.recent.Len()
    freqLen := c.frequent.Len()
    if recentLen+freqLen < c.size {
        return
    }

    // If the recent buffer is larger than
    // the target, evict from there
    if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
        k, _, _ := c.recent.RemoveOldest()
        c.recentEvict.Add(k, nil)
        return
    }

    // Remove from the frequent list otherwise
    c.frequent.RemoveOldest()
}

如果recentLen+freqLen < size,不需要任何操作,否则:
- 如果recentLen > recentSize,recent RemoveOldest
- 如果recentLen < recentSize,frequent RemoveOldest
- 如果recentLen == recentSize,通过参数指定清理哪个队列

注意:以上逻辑能保证缓存总长度不超过size。但是并不保证两个队列长度按初始化比率分配,当然这也是符合预期的。例如:

当size=4,recentSize 和 frequentSize都是2的缓存,初始化之后,按顺序添加A,B,A,B,C,D,C,D

你可能感兴趣的:(golang)