您好,登錄后才能下訂單哦!
這篇文章主要介紹了Go語言如何實(shí)現(xiàn)LRU算法的核心思想和實(shí)現(xiàn)過程,具有一定借鑒價(jià)值,需要的朋友可以參考下。下面就和我一起來看看吧。
常見的三種緩存淘汰算法有三種:FIFO,LRU和LFU
實(shí)現(xiàn)LRU緩存淘汰算法
緩存全部存在內(nèi)存中,內(nèi)存是有限的,因此不可能無限制的添加數(shù)據(jù),假定我們能夠最大使用的內(nèi)存為Max,那么在一個(gè)時(shí)間點(diǎn)之后,添加了某一條緩存記錄之后,占用內(nèi)存超過了N,這個(gè)時(shí)候就需要從緩存中移除一些數(shù)據(jù),那么該移除或者淘汰誰呢?我們肯定希望去移除沒用的數(shù)據(jù),將熱點(diǎn)數(shù)據(jù)留在緩存中,下面幾種就是對(duì)應(yīng)的幾種策略!
FIFO (First in First Out)
先進(jìn)先出,內(nèi)存滿了時(shí)淘汰最老存在最久的key緩存,這種算法認(rèn)為越早被添加的數(shù)據(jù)使用可能性會(huì)比新加入進(jìn)來的小,但是這種也有弊端,在某些場景下比如查歷史支付記錄的賬單,就需要查詢之前的緩存記錄,這類記錄就不得不因?yàn)槊刻煨碌闹Ц稄亩蕴粢郧暗闹Ц队涗洠ó?dāng)然這類記錄存庫是最好方式)
【實(shí)現(xiàn)方式】維護(hù)一個(gè)隊(duì)列先進(jìn)先出就行了,新來的數(shù)據(jù)加到隊(duì)尾
LFU (Least Frequently Used)
最少使用,也就是淘汰緩存中訪問頻率最低的記錄。這個(gè)算法認(rèn)為過去訪問次數(shù)最高的使用概率也最大,但是這樣就額外維護(hù)了一個(gè)訪問次數(shù),對(duì)內(nèi)存其實(shí)占用也挺多的,再者訪問次數(shù)最多的數(shù)據(jù),突然不使用了,那么要淘汰它之前,需要內(nèi)存其他區(qū)域的數(shù)據(jù)訪問次數(shù)全部超過它才有可能,所以淘汰的缺點(diǎn)也非常明顯。
【實(shí)現(xiàn)方式】LFU 的實(shí)現(xiàn)需要維護(hù)一個(gè)按照訪問次數(shù)排序的隊(duì)列,每次訪問,訪問次數(shù)加1,隊(duì)列重新排序,淘汰時(shí)選擇訪問次數(shù)最少的即可
LRU (Least Recently Used)
最近最少使用,相對(duì)于只考慮使用時(shí)間和使用次數(shù)來看,LRU會(huì)相對(duì)比較平均去淘汰數(shù)據(jù),如果數(shù)據(jù)最近被使用過,那么將來被訪問的概率將提高
【實(shí)現(xiàn)方式】維護(hù)一個(gè)隊(duì)列,如果某條記錄被訪問了,則移動(dòng)到隊(duì)尾,那么隊(duì)首則是最近最少訪問的數(shù)據(jù),淘汰該條記錄即可。
這張圖很好地表示了 LRU 算法最核心的 2 個(gè)數(shù)據(jù)結(jié)構(gòu)
綠色的是字典(map),存儲(chǔ)鍵和值的映射關(guān)系。這樣根據(jù)某個(gè)鍵(key)查找對(duì)應(yīng)的值(value)的復(fù)雜是O(1)
,在字典中插入一條記錄的復(fù)雜度也是O(1)
。
紅色的是雙向鏈表(double linked list)實(shí)現(xiàn)的隊(duì)列。將所有的值放到雙向鏈表中,這樣,當(dāng)訪問到某個(gè)值時(shí),將其移動(dòng)到隊(duì)尾的復(fù)雜度是O(1)
,在隊(duì)尾新增一條記錄以及刪除一條記錄的復(fù)雜度均為O(1)
。
接下來我們創(chuàng)建一個(gè)包含字典和雙向鏈表的結(jié)構(gòu)體類型 Cache,方便實(shí)現(xiàn)后續(xù)的增刪查改操作。
package lru import ( "container/list" "log" ) // Cache is a LRU cache. It is not safe for concurrent access. type Cache struct { maxBytes int64 // 允許使用的最大內(nèi)存 nbytes int64 // 當(dāng)前已使用的內(nèi)存 ll *list.List cache map[string]*list.Element // optional and executed when an entry is purged. OnEvicted func(key string, value Value) // 某條記錄被移除時(shí)的回調(diào)函數(shù) } type entry struct { key string value Value } // Value use Len to count how many bytes it takes type Value interface { Len() int }
在這里我們直接使用 Go 語言標(biāo)準(zhǔn)庫實(shí)現(xiàn)的雙向鏈表list.List
。
字典的定義是 map[string]*list.Element
,鍵是字符串,值是雙向鏈表中對(duì)應(yīng)節(jié)點(diǎn)的指針。
maxBytes
是允許使用的最大內(nèi)存,nbytes
是當(dāng)前已使用的內(nèi)存,OnEvicted
是某條記錄被移除時(shí)的回調(diào)函數(shù),可以為 nil。
鍵值對(duì) entry
是雙向鏈表節(jié)點(diǎn)的數(shù)據(jù)類型,在鏈表中仍保存每個(gè)值對(duì)應(yīng)的 key 的好處在于,淘汰隊(duì)首節(jié)點(diǎn)時(shí),需要用 key 從字典中刪除對(duì)應(yīng)的映射。
為了通用性,我們允許值是實(shí)現(xiàn)了 Value
接口的任意類型,該接口只包含了一個(gè)方法 Len() int
,用于返回值所占用的內(nèi)存大小。
方便實(shí)例化 Cache
,實(shí)現(xiàn) New()
函數(shù):
// New is the Constructor of Cache func New(maxBytes int64, onEvicted func(string, Value)) *Cache { return &Cache{ maxBytes: maxBytes, ll: list.New(), cache: make(map[string]*list.Element), OnEvicted: onEvicted, } }
查找主要有 2 個(gè)步驟,第一步是從字典中找到對(duì)應(yīng)的雙向鏈表的節(jié)點(diǎn),第二步,將該節(jié)點(diǎn)移動(dòng)到隊(duì)尾。
func (c *Cache)Get(key string)(val Value,ok bool){ // 如果找到了節(jié)點(diǎn),就將緩存移動(dòng)到隊(duì)尾 if ele,ok:=c.cache[key];ok{ c.ll.MoveToBack(ele) kv:=ele.Value.(*entry) return kv.value,true } return }
如果鍵對(duì)應(yīng)的鏈表節(jié)點(diǎn)存在,則將對(duì)應(yīng)節(jié)點(diǎn)移動(dòng)到隊(duì)尾,并返回查找到的值。
c.ll.MoveToBack(ele)
,即將鏈表中的節(jié)點(diǎn) ele
移動(dòng)到隊(duì)尾
這里的刪除,實(shí)際上是緩存淘汰。即移除最近最少訪問的節(jié)點(diǎn)(隊(duì)首)
// 移除最近最近,最少訪問的的節(jié)點(diǎn) func (c *Cache)RemoveOldest(){ ele:=c.ll.Front() // 取到隊(duì)首節(jié)點(diǎn),從鏈表中刪除 if ele!=nil{ c.ll.Remove(ele) kv:=ele.Value.(*entry) delete(c.cache,kv.key) c.nbytes-=int64(len(kv.key))+int64(kv.value.Len()) if c.OnEvicted != nil { c.OnEvicted(kv.key, kv.value) } } }
c.ll.Front()
取到隊(duì)首節(jié)點(diǎn),從鏈表中刪除。
delete(c.cache, kv.key)
,從字典中 c.cache
刪除該節(jié)點(diǎn)的映射關(guān)系。
更新當(dāng)前所用的內(nèi)存 c.nbytes
。
如果回調(diào)函數(shù) OnEvicted
不為 nil,則調(diào)用回調(diào)函數(shù)
// 新增或修改 func (c *Cache)Add(key string ,value Value){ if int64(value.Len())>c.maxBytes{ log.Printf("超過最大容量%d,插入緩存容量為%d",c.maxBytes,int64(value.Len())) return } if ele,ok:=c.cache[key];ok{ c.ll.MoveToBack(ele) kv:=ele.Value.(*entry) c.nbytes += int64(value.Len()) - int64(kv.value.Len()) }else{ ele:=c.ll.PushFront(&entry{key: key,value: value}) c.cache[key]=ele c.nbytes=int64(len(key))+int64(value.Len()) } // 如果當(dāng)前使用的內(nèi)存>允許使用的最大內(nèi)存 使用淘汰策略 for c.maxBytes !=0 && c.maxBytes < c.nbytes{ c.RemoveOldest() } }
如果鍵存在,則更新對(duì)應(yīng)節(jié)點(diǎn)的值,并將該節(jié)點(diǎn)移到隊(duì)尾。
不存在則是新增場景,首先隊(duì)尾添加新節(jié)點(diǎn) &entry{key, value}
, 并字典中添加 key 和節(jié)點(diǎn)的映射關(guān)系。
更新 c.nbytes
,如果超過了設(shè)定的最大值 c.maxBytes
,則移除最少訪問的節(jié)點(diǎn)。
以上就是Go語言如何實(shí)現(xiàn)LRU算法的核心思想和實(shí)現(xiàn)過程的詳細(xì)內(nèi)容了,看完之后是否有所收獲呢?如果想了解更多相關(guān)內(nèi)容,歡迎來億速云行業(yè)資訊!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。