groupcache源碼分析一:LRU

1.LRU簡介

LRU(Least Recently Used)是一種緩存淘汰策略.
受內存大小的限制,不能將所有的數據都緩存在內存中,當緩存超過規定的容量時,再往裏面加數據就要考慮將誰先換出去,即淘汰掉.
LRU的做法是:淘汰最近最少使用的數據.
LRU可以通過哈希表+雙向鏈表實現,雙向鏈表的每個結點中存儲{key,value},哈希表中存儲{key,key所在的結點}.

具體實現可參考:leetcode146——LRU 緩存機制
內存結構示意圖

LRU最主要的兩個操作爲getput(或add),其中get從緩存中獲取key對應的value,put/add將新的數據加入緩存當中,如果加入過程中超出緩存的容量,將會導致鍵的淘汰.

get操作

if key不存在:
	直接返回-1
else 
	在原鏈表中刪除(key,value)(key,value)重新放回鏈表的頭部
	更新哈希表
	返回value

put操作

if key存在:
	在原鏈表中刪除(key, value)(key,value)重新放回鏈表的頭部
	更新哈希表
else
	if 鏈表長度達到上限:
		獲取鏈表尾部的key
		在哈希表中刪除key
		刪除鏈表尾部元素
		將新的(key,value)插入鏈表頭部
		將(key, key在鏈表中的位置)放入哈希表
	else
		將新的(key,value)插入鏈表頭部
		將(key, key在鏈表中的位置)放入哈希表

2. groupcache中的LRU

groupcache中的LRU也是通過哈希表加雙向鏈表實現的,具體實現在lru目錄下.

2.1 cache結構

import "container/list" //雙向鏈表

type Cache struct {
	MaxEntries int // MaxEntries表示鏈表最多能容納的結點數量,如果該字段爲0說明容量沒有限制
	OnEvicted func(key Key, value interface{}) // 當緩存中的一個結點被刪除時,調用該函數

	ll    *list.List //雙向鏈表
	cache map[interface{}]*list.Element // 哈希表
}

// A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators
type Key interface{} // key必須是可以比較的,因爲要在map中當key

type entry struct { //鏈表中每個結點的結構,key和value都是interface
	key   Key
	value interface{}
}

Cache即爲LRU緩存,struct中包含了雙向鏈表和哈希表,以及最大容量和結點被刪除時調用的函數.
如果函數在初始化時沒有指定,則不調用.

2.2 創建Cache New

func New(maxEntries int) *Cache { // 創建一個Cache
	return &Cache{
		MaxEntries: maxEntries,
		ll:         list.New(),
		cache:      make(map[interface{}]*list.Element),
	}
}

返回值爲指針類型.

2.3 get操作

函數原形:func (c *Cache) Get(key Key) (value interface{}, ok bool)
參數:key表示要查找的鍵
返回值:valuekey對應的值,ok表示是否命中,true表示命中.

func (c *Cache) Get(key Key) (value interface{}, ok bool) {
	if c.cache == nil { // 緩存爲空直接返回
		return
	}
	if ele, hit := c.cache[key]; hit { // ele表示key在鏈表中對應的結點,hit表示是否命中
		c.ll.MoveToFront(ele) //命中,說明緩存中有key,則將key對應的結點移動到鏈表頭部(因爲剛剛被訪問)
		return ele.Value.(*entry).value, true // 返回key對應的值, true表示命中
	}
	return
}

2.4 add操作

函數原形: func (c *Cache) Add(key Key, value interface{})
參數: 新加入的keyvalue
返回值: 無

func (c *Cache) Add(key Key, value interface{}) { // 添加{key,value}到緩存中
	if c.cache == nil { // 如果之前緩存爲空, 則先創建哈希表和鏈表
		c.cache = make(map[interface{}]*list.Element)
		c.ll = list.New()
	}
	if ee, ok := c.cache[key]; ok { // 如果key原來就有, 我們只需要更新key對應的值即可
		c.ll.MoveToFront(ee) // 將key對應的結點移動至鏈表頭部, 因爲剛剛被訪問過
		ee.Value.(*entry).value = value // 更新key對應的value, 然後返回
		return
	}
	ele := c.ll.PushFront(&entry{key, value}) // 如果key不存在, 則創建一個結點並將其放在鏈表頭部
	c.cache[key] = ele // 更新哈希表
	if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { // 如果插入新結點後超過最大容量, 則淘汰一個鍵
		c.RemoveOldest() // 用於淘汰最近最少使用的
	}
}

重點要理清add操作的邏輯流程.

2.5 RemoveOldest 刪除操作

刪除最近最少使用的item的操作由函數RemoveOldest完成.
RemoveOldest首先獲取最近最少使用的結點,然後調用函數removeElement將其刪除.

刪除過程包括: 從鏈表中刪除對應結點, 從哈希表中刪除對應項.

// RemoveOldest removes the oldest item from the cache.
func (c *Cache) RemoveOldest() { // 刪除最近最久沒有使用的
	if c.cache == nil {
		return
	}
	ele := c.ll.Back() // 鏈表尾部的結點就是最近最少使用的
	if ele != nil {
		c.removeElement(ele) // 調用removeElement函數刪除item
	}
}

func (c *Cache) removeElement(e *list.Element) {
	c.ll.Remove(e) // 從鏈表中刪除key對應的結點
	kv := e.Value.(*entry)
	delete(c.cache, kv.key) // 從哈希表中刪除key
	if c.OnEvicted != nil { // 如果OnEvicted被設置過, 則在刪除item時要調用一個這個函數
		c.OnEvicted(kv.key, kv.value)
	}
}

2.6 清空cache

// Clear purges all stored items from the cache.
func (c *Cache) Clear() { // 清空cache, 刪除所有的結點
	if c.OnEvicted != nil { // 如果OnEvicted被設置, 則刪除時需要調用一個這個函數
		for _, e := range c.cache {
			kv := e.Value.(*entry)
			c.OnEvicted(kv.key, kv.value)
		}
	}
	c.ll = nil // 然後將鏈表清空
	c.cache = nil // 將哈希表清空
}

3. 總結

lru是groupcache中基礎且簡單的內容,如果瞭解lru算法,實現一個lru並不是特別難,但通過閱讀源碼,可以鞏固語法.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章