[TOC]

golang-lru包学习 - 2Q算法

2Q算法

代码:2Q
论文:A low overhead high performance buffer replacement algorithm

2Q算法的描述:

2Q与LRU/2对于LRU的改进在某种程度上存在互补性,2Q并不是通过直接将冷页从主缓存中剔出,而是通过将热页换入的形式来实现。与LRU/2算法类似,2Q以该页的第二次被访问时间来计算。简单来说,在某一页第一次被访问的时候,2Q将其放入一个特殊的缓冲区,称为A1 queue,其实现就是一个FIFO队列,如果该页在A1 queue的生存周期内被再次访问,则可能是热页,将其置入Am queue,其实就是一个LRU。如果在A1 queue的生存期内没有再次被访问到,则将其换出。

理论简易版本2Q算法实现:

首次添加  Add("A")
          | A1(FIFO) |           | Am (LRU)|
          |----------|           |---------|
ele(A)->  | ele(A)   |           |         |
          |----------|           |---------|
		
第二次添加或者访问 Add("A") Get("A")
          | A1(FIFO) |           | Am (LRU)|
	  |----------|           |---------|
          |          |    ->     | ele(A)  |
	  |----------|           |---------|

理论2Q算法实现:

首次添加  Add("A")
          | A1in(FIFO)|      |A1out(FIFO)|       | Am (LRU)|
          |-----------|      |-----------|       |---------|
ele(A)->  | ele(A)    |      |           |       |         |
	  |-----------|      |-----------|       |---------|
		
第二次添加或者访问 Add("A") Get("A")
          | A1in(FIFO)|      |A1out(FIFO)|       | Am (LRU)|
	  |-----------|      |-----------|       |---------|
          |           |  ->  |   ele(A)  |       |         |
          |-----------|      |-----------|       |---------|
		
第三次添加或者访问 Add("A") Get("A")
          | A1in(FIFO)|      |A1out(FIFO)|       | Am (LRU)|
	  |-----------|      |-----------|       |---------|
          |           |      |           |   ->  | ele(A)  |
	  |-----------|      |-----------|       |---------|		

2Q算法特点是:

结合FIFO和LRU特性,把新加入的条目放到FIFO,如果有二次访问再换入LRU队列中,确保LRU队列中的都是热页,减少偶发性数据的影响,其添加、删除、查找操作都需要分别对FIFO和LRU两个队列进行执行。

理论上2Q算法设计中使用的是FIFO队列进行,但在当前包中FIFO队列使用的与LRU队列一样都是simple-lru。另外一个特点是,本包的2Q算法是基于simple版本的2Q算法,包含A1和Am队列,但是增加了A1淘汰队列,对于从A1中淘汰下来的元素会添加到A1evict队列中,当缓存再次添加A1evict中的元素时候,元素不会重新进入A1队列,而是直接进入LRU队列中。

需要注意的是2Q算法的三个队列长度具有比例关系,初始时候需要设置队列大小比例,默认比例关系: LRU:A1:A1evict = 1:0.5:0.25 队列大小比例设置会影响冷页的置换。

recent -> A1
recentEvict -> A1淘汰队列 frequent -> Am

本包2Q算法实现:

首次添加  Add("A")
          | A1(LRU)   |      |A1evict(LRU)|       | Am (LRU)|
          |-----------|      |------------|       |---------|
ele(A)->  | ele(A)    |      |            |       |         |
          |-----------|      |------------|       |---------|
		
第二次添加或者访问 Add("A") Get("A")
          | A1(LRU)   |      |A1evict(LRU)|       | Am (LRU)|
          |-----------|      |------------|       |---------|
          |           |      |            |   ->  | ele(A)  |
	  |-----------|      |------------|       |---------|
		
对于已被A1淘汰的数据,再次添加访问 Add("A")
          | A1(LRU)   |       |A1evict(LRU)|       | Am (LRU)|
          |-----------|      |------------|       |---------|
          |           |      |    ele(A)  |       |         |
	  |-----------|      |------------|       |---------|		
		
          | A1(LRU)   |      |A1evict(LRU)|       | Am (LRU)|
	  |-----------|      |------------|       |---------|
          |           |      |            |   ->  | ele(A)  |
	  |-----------|      |------------|       |---------|
type TwoQueueCache struct {
	size       int					// 队列既定长度
	recentSize int					// FIFO队列既定长度

	recent      simplelru.LRUCache			// A1队列
	recentEvict simplelru.LRUCache			// A1淘汰队列
	frequent    simplelru.LRUCache			// LRU队列
	lock        sync.RWMutex			// 读写锁
}

函数

type 2QCache interface {
  New2Q(size int) (*TwoQueueCache, error) 				// 创建一个2Q算法的缓存对象,队列长度参数使用默认参数 1:0.5:0.25
  New2QParams(size int, recentRatio float64, ghostRatio float64) (*TwoQueueCache, error) // 创建一个2Q算法的缓存对象,队列长度参数使用自定义参数
  Add(key, value interface{})  				// 添加元素,若元素不存在则添加元素,若元素存在则更新元素
  Get(key interface{}) (value interface{}, ok bool) 	// 获取元素
  Contains(key interface{}) (ok bool) 			// 查看该元素是否存在,但不会更新元素位置
  Peek(key interface{}) (value interface{}, ok bool)	// 获取元素,但不会更新元素位置
  Remove(key interface{}) bool				// 删除元素
  Len() int						// 返回队列中元素数量
  Purge()						// 清除队列中全部元素
}

过程

2Q算法核心过程是Get和Add,我们针对这两个方法进到代码里看看

Add() 添加元素

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

	if c.frequent.Contains(key) {					// 如果在LRU队列,直接更新
		c.frequent.Add(key, value)
		return
	}

	if c.recent.Contains(key) {					// 如果在A1队列,则移动到LRU队列
		c.recent.Remove(key)
		c.frequent.Add(key, value)
		return
	}

	if c.recentEvict.Contains(key) {				// 如果在A1淘汰队列,则移动到LRU队列
		c.ensureSpace(true)					// 对执行淘汰,腾出空间 1.对于A1in队列如果超出预设长度,则淘汰末端到Alevict 2.对于LRU队列,直接淘汰末端
		c.recentEvict.Remove(key)
		c.frequent.Add(key, value)
		return
	}

	c.ensureSpace(false)						// 对执行淘汰,腾出空间 1.对于LRU队列,直接淘汰末端
	c.recent.Add(key, value)					// 添加元素到A1队列
	return
}

Get() 获取元素

func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) {
	c.lock.Lock()
	defer c.lock.Unlock()

	if val, ok := c.frequent.Get(key); ok {			// 从LRU队列获取
		return val, ok
	}

	if val, ok := c.recent.Peek(key); ok {			// 从A1in队列获取
		c.recent.Remove(key)				// 获取成功,移动到LRU队列
		c.frequent.Add(key, val)
		return val, ok
	}

	// No hit
	return nil, false
}

总结

  • 2Q算法实现是基于simple 2Q算法,底层包括 A1、Am和A1淘汰队列
  • 2Q算法可以减弱偶发性的数据对缓存的影响
  • 2Q算法需要设置队列长度比例

文章目录