1.LRU簡介
LRU(Least Recently Used)是一種緩存淘汰策略.
受內存大小的限制,不能將所有的數據都緩存在內存中,當緩存超過規定的容量時,再往裏面加數據就要考慮將誰先換出去,即淘汰掉.
LRU的做法是:淘汰最近最少使用的數據.
LRU可以通過哈希表+雙向鏈表實現,雙向鏈表的每個結點中存儲{key,value},哈希表中存儲{key,key所在的結點}.
具體實現可參考:leetcode146——LRU 緩存機制
內存結構示意圖
LRU最主要的兩個操作爲get
和put
(或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
表示要查找的鍵
返回值:value
爲key
對應的值,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{})
參數: 新加入的key
和value
返回值: 無
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並不是特別難,但通過閱讀源碼,可以鞏固語法.