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)
}
来源:CSDN
作者:RRRagnaros
链接:https://blog.csdn.net/weixin_42609341/article/details/104093178