緩存淘汰算法--LRU算法

1. LRU

1.1.?原理

LRU(Least?recently?used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”。

1.2.?實現(xiàn)

最常見的實現(xiàn)是使用一個鏈表保存緩存數(shù)據(jù),詳細算法實現(xiàn)如下:

1.?新數(shù)據(jù)插入到鏈表頭部;

2.?每當緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表頭部;

3.?當鏈表滿的時候,將鏈表尾部的數(shù)據(jù)丟棄。

1.3.?分析

【命中率】

當存在熱點數(shù)據(jù)時,LRU的效率很好,但偶發(fā)性的、周期性的批量操作會導(dǎo)致LRU命中率急劇下降,緩存污染情況比較嚴重。

【復(fù)雜度】

實現(xiàn)簡單。

【代價】

命中時需要遍歷鏈表,找到命中的數(shù)據(jù)塊索引,然后需要將數(shù)據(jù)移到頭部。

2.?LRU-K

2.1.?原理

LRU-K中的K代表最近使用的次數(shù),因此LRU可以認為是LRU-1。LRU-K的主要目的是為了解決LRU算法“緩存污染”的問題,其核心思想是將“最近使用過1次”的判斷標準擴展為“最近使用過K次”。

2.2.?實現(xiàn)

相比LRU,LRU-K需要多維護一個隊列,用于記錄所有緩存數(shù)據(jù)被訪問的歷史。只有當數(shù)據(jù)的訪問次數(shù)達到K次的時候,才將數(shù)據(jù)放入緩存。當需要淘汰數(shù)據(jù)時,LRU-K會淘汰第K次訪問時間距當前時間最大的數(shù)據(jù)。詳細實現(xiàn)如下:

1.?數(shù)據(jù)第一次被訪問,加入到訪問歷史列表;

2.?如果數(shù)據(jù)在訪問歷史列表里后沒有達到K次訪問,則按照一定規(guī)則(FIFO,LRU)淘汰;

3.?當訪問歷史隊列中的數(shù)據(jù)訪問次數(shù)達到K次后,將數(shù)據(jù)索引從歷史隊列刪除,將數(shù)據(jù)移到緩存隊列中,并緩存此數(shù)據(jù),緩存隊列重新按照時間排序;

4.?緩存數(shù)據(jù)隊列中被再次訪問后,重新排序;

5.?需要淘汰數(shù)據(jù)時,淘汰緩存隊列中排在末尾的數(shù)據(jù),即:淘汰“倒數(shù)第K次訪問離現(xiàn)在最久”的數(shù)據(jù)。

LRU-K具有LRU的優(yōu)點,同時能夠避免LRU的缺點,實際應(yīng)用中LRU-2是綜合各種因素后最優(yōu)的選擇,LRU-3或者更大的K值命中率會高,但適應(yīng)性差,需要大量的數(shù)據(jù)訪問才能將歷史訪問記錄清除掉。

2.3.?分析

【命中率】

LRU-K降低了“緩存污染”帶來的問題,命中率比LRU要高。

【復(fù)雜度】

LRU-K隊列是一個優(yōu)先級隊列,算法復(fù)雜度和代價比較高。

【代價】

由于LRU-K還需要記錄那些被訪問過、但還沒有放入緩存的對象,因此內(nèi)存消耗會比LRU要多;當數(shù)據(jù)量很大的時候,內(nèi)存消耗會比較可觀。

LRU-K需要基于時間進行排序(可以需要淘汰時再排序,也可以即時排序),CPU消耗比LRU要高。

3.?Two?queues(2Q)

3.1.?原理

Two?queues(以下使用2Q代替)算法類似于LRU-2,不同點在于2Q將LRU-2算法中的訪問歷史隊列(注意這不是緩存數(shù)據(jù)的)改為一個FIFO緩存隊列,即:2Q算法有兩個緩存隊列,一個是FIFO隊列,一個是LRU隊列。

3.2.?實現(xiàn)

當數(shù)據(jù)第一次訪問時,2Q算法將數(shù)據(jù)緩存在FIFO隊列里面,當數(shù)據(jù)第二次被訪問時,則將數(shù)據(jù)從FIFO隊列移到LRU隊列里面,兩個隊列各自按照自己的方法淘汰數(shù)據(jù)。詳細實現(xiàn)如下:

1.?新訪問的數(shù)據(jù)插入到FIFO隊列;

2.?如果數(shù)據(jù)在FIFO隊列中一直沒有被再次訪問,則最終按照FIFO規(guī)則淘汰;

3.?如果數(shù)據(jù)在FIFO隊列中被再次訪問,則將數(shù)據(jù)移到LRU隊列頭部;

4.?如果數(shù)據(jù)在LRU隊列再次被訪問,則將數(shù)據(jù)移到LRU隊列頭部;

5.?LRU隊列淘汰末尾的數(shù)據(jù)。

注:上圖中FIFO隊列比LRU隊列短,但并不代表這是算法要求,實際應(yīng)用中兩者比例沒有硬性規(guī)定。

3.3.?分析

【命中率】

2Q算法的命中率要高于LRU。

【復(fù)雜度】

需要兩個隊列,但兩個隊列本身都比較簡單。

【代價】

FIFO和LRU的代價之和。

2Q算法和LRU-2算法命中率類似,內(nèi)存消耗也比較接近,但對于最后緩存的數(shù)據(jù)來說,2Q會減少一次從原始存儲讀取數(shù)據(jù)或者計算數(shù)據(jù)的操作。

4.?Multi?Queue(MQ)

4.1.?原理

MQ算法根據(jù)訪問頻率將數(shù)據(jù)劃分為多個隊列,不同的隊列具有不同的訪問優(yōu)先級,其核心思想是:優(yōu)先緩存訪問次數(shù)多的數(shù)據(jù)。

4.2.?實現(xiàn)

MQ算法將緩存劃分為多個LRU隊列,每個隊列對應(yīng)不同的訪問優(yōu)先級。訪問優(yōu)先級是根據(jù)訪問次數(shù)計算出來的,例如

詳細的算法結(jié)構(gòu)圖如下,Q0,Q1....Qk代表不同的優(yōu)先級隊列,Q-history代表從緩存中淘汰數(shù)據(jù),但記錄了數(shù)據(jù)的索引和引用次數(shù)的隊列:

如上圖,算法詳細描述如下:

1.?新插入的數(shù)據(jù)放入Q0;

2.?每個隊列按照LRU管理數(shù)據(jù);

3.?當數(shù)據(jù)的訪問次數(shù)達到一定次數(shù),需要提升優(yōu)先級時,將數(shù)據(jù)從當前隊列刪除,加入到高一級隊列的頭部;

4.?為了防止高優(yōu)先級數(shù)據(jù)永遠不被淘汰,當數(shù)據(jù)在指定的時間里訪問沒有被訪問時,需要降低優(yōu)先級,將數(shù)據(jù)從當前隊列刪除,加入到低一級的隊列頭部;

5.?需要淘汰數(shù)據(jù)時,從最低一級隊列開始按照LRU淘汰;每個隊列淘汰數(shù)據(jù)時,將數(shù)據(jù)從緩存中刪除,將數(shù)據(jù)索引加入Q-history頭部;

6.?如果數(shù)據(jù)在Q-history中被重新訪問,則重新計算其優(yōu)先級,移到目標隊列的頭部;

7.?Q-history按照LRU淘汰數(shù)據(jù)的索引。

4.3.?分析

【命中率】

MQ降低了“緩存污染”帶來的問題,命中率比LRU要高。

【復(fù)雜度】

MQ需要維護多個隊列,且需要維護每個數(shù)據(jù)的訪問時間,復(fù)雜度比LRU高。

【代價】

MQ需要記錄每個數(shù)據(jù)的訪問時間,需要定時掃描所有隊列,代價比LRU要高。

注:雖然MQ的隊列看起來數(shù)量比較多,但由于所有隊列之和受限于緩存容量的大小,因此這里多個隊列長度之和和一個LRU隊列是一樣的,因此隊列掃描性能也相近。

5.?LRU類算法對比

由于不同的訪問模型導(dǎo)致命中率變化較大,此處對比僅基于理論定性分析,不做定量分析。

對比點

對比

命中率

LRU-2?>?MQ(2)?>?2Q?>?LRU

復(fù)雜度

LRU-2?>?MQ(2)?>?2Q?>?LRU

代價

LRU-2??>?MQ(2)?>?2Q?>?LRU

實際應(yīng)用中需要根據(jù)業(yè)務(wù)的需求和對數(shù)據(jù)的訪問情況進行選擇,并不是命中率越高越好。例如:雖然LRU看起來命中率會低一些,且存在”緩存污染“的問題,但由于其簡單和代價小,實際應(yīng)用中反而應(yīng)用更多。

java中最簡單的LRU算法實現(xiàn),就是利用jdk的LinkedHashMap,覆寫其中的removeEldestEntry(Map.Entry)方法即可

如果你去看LinkedHashMap的源碼可知,LRU算法是通過雙向鏈表來實現(xiàn),當某個位置被命中,通過調(diào)整鏈表的指向?qū)⒃撐恢谜{(diào)整到頭位置,新加入的內(nèi)容直接放在鏈表頭,如此一來,最近被命中的內(nèi)容就向鏈表頭移動,需要替換時,鏈表最后的位置就是最近最少使用的位置。

import java.util.ArrayList;?

?import java.util.Collection;?

?import java.util.LinkedHashMap;?

?import java.util.concurrent.locks.Lock;?

?import java.util.concurrent.locks.ReentrantLock;?

?import java.util.Map;? ? ??

? /**? * 類說明:利用LinkedHashMap實現(xiàn)簡單的緩存, 必須實現(xiàn)removeEldestEntry方法,具體參見JDK文檔? *??

?*?@author dennis? *??

?* @param*?

@param*/?

public class LRULinkedHashMapextends LinkedHashMap{? ? ??

private final int maxCapacity;? ? ? ?

?private static final float DEFAULT_LOAD_FACTOR = 0.75f;? ? ? ?

?private final Lock lock = new ReentrantLock();? ? ? ??

?public LRULinkedHashMap(int maxCapacity) {? ? ? ? ?

?super(maxCapacity, DEFAULT_LOAD_FACTOR, true);? ? ? ? ??

this.maxCapacity = maxCapacity;? ? ? }? ? ? ??

?@Override? ??

?protected boolean removeEldestEntry(java.util.Map.Entryeldest) {? ? ? ? ?

?return size() > maxCapacity;? ??

? }? ? ??

@Override? ??

?public boolean containsKey(Object key) {? ? ? ? ??

try {? ? ? ? ? ? ??

lock.lock();? ? ? ? ? ? ??

return super.containsKey(key);? ? ? ? ?

?} finally {? ? ? ? ? ? ??

lock.unlock();? ? ? ? ?

?}? ? ??

}? ? ? ? ? ? ? ??

@Override? ??

?public V get(Object key) {? ? ? ? ?

?try {? ? ? ? ? ? ??

lock.lock();? ? ? ? ? ? ??

return super.get(key);? ? ? ? ?

?} finally {? ? ? ? ? ? ?

?lock.unlock();? ? ? ? ??

}? ? ?

?}? ? ? ??

?@Override? ?

?public V put(K key, V value) {? ? ? ? ?

?try {? ? ? ? ? ? ??

lock.lock();? ? ? ? ? ? ??

return super.put(key, value);? ? ? ? ?

?} finally {? ? ? ? ? ? ??

lock.unlock();? ? ? ??

? }? ? ?

?}? ? ??

? public int size() {? ? ? ? ?

?try { ? ? ? ? ? ??

lock.lock();? ? ? ? ? ? ?

?return super.size();? ? ? ? ?

?} finally {? ? ? ? ? ? ?

?lock.unlock();? ? ? ? ??

}? ? ??

}? ? ? ??

?public void clear() {? ? ? ? ??

try {? ? ? ? ? ? ??

lock.lock();? ? ? ? ? ? ?

?super.clear();? ? ? ? ?

?} finally {? ? ? ? ? ? ??

lock.unlock();? ? ? ? ?

?}? ? ?

?}? ? ? ?

?public Collection> getAll() {? ? ? ? ??

try {? ? ? ? ? ? ??

lock.lock();? ? ? ? ? ? ??

return new ArrayList>(super.entrySet());

} finally {

lock.unlock();

}

}

}

基于雙鏈表 的LRU實現(xiàn):

傳統(tǒng)意義的LRU算法是為每一個Cache對象設(shè)置一個計數(shù)器,每次Cache命中則給計數(shù)器+1,而Cache用完,需要淘汰舊內(nèi)容,放置新內(nèi)容時,就查看所有的計數(shù)器,并將最少使用的內(nèi)容替換掉。

它的弊端很明顯,如果Cache的數(shù)量少,問題不會很大, 但是如果Cache的空間過大,達到10W或者100W以上,一旦需要淘汰,則需要遍歷所有計算器,其性能與資源消耗是巨大的。效率也就非常的慢了。

它的原理: 將Cache的所有位置都用雙連表連接起來,當一個位置被命中之后,就將通過調(diào)整鏈表的指向,將該位置調(diào)整到鏈表頭的位置,新加入的Cache直接加到鏈表頭中。

這樣,在多次進行Cache操作后,最近被命中的,就會被向鏈表頭方向移動,而沒有命中的,而想鏈表后面移動,鏈表尾則表示最近最少使用的Cache。

當需要替換內(nèi)容時候,鏈表的最后位置就是最少被命中的位置,我們只需要淘汰鏈表最后的部分即可。

上面說了這么多的理論, 下面用代碼來實現(xiàn)一個LRU策略的緩存。

我們用一個對象來表示Cache,并實現(xiàn)雙鏈表,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. LRU 1.1. 原理LRU(Least recently used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記...
    AKyS佐毅閱讀 2,196評論 0 3
  • 緩存淘汰算法--LRU算法 1. LRU 1.1 原理 LRU(Least recently used,)算法根據(jù)...
    白公子是貓奴閱讀 483評論 0 0
  • LRU原理 LRU(Least recently used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù)...
    jiangmo閱讀 60,285評論 3 30
  • Android緩存淺析 By吳思博 1、引言 2、常見的幾種緩存算法 3、Android緩存的機制 4、LruCa...
    吳小博Toby閱讀 2,930評論 1 5
  • 場景二: 白:老師布置任務(wù)了,是做一個todolist 黃:todolist是什么 白:好像是一個添加任務(wù)的工具吧...
    baiying閱讀 515評論 0 0