快取演算法(FIFO 、LRU、LFU三種演算法的區別)
FIFO演算法#
FIFO 演算法是一種比較容易實現的演算法。它的思想是先進先出(FIFO,佇列),這是最簡單、最公平的一種思想,即如果一個數據是最先進入的,那麼可以認為在將來它被訪問的可能性很小。空間滿的時候,最先進入的資料會被最早置換(淘汰)掉。
FIFO 演算法的描述:設計一種快取結構,該結構在構造時確定大小,假設大小為 K,並有兩個功能:
- set(key,value):將記錄(key,value)插入該結構。當快取滿時,將最先進入快取的資料置換掉。
- get(key):返回key對應的value值。
實現:維護一個FIFO佇列,按照時間順序將各資料(已分配頁面)連結起來組成佇列,並將置換指標指向佇列的隊首。再進行置換時,只需把置換指標所指的資料(頁面)順次換出,並把新加入的資料插到隊尾即可。
缺點:判斷一個頁面置換演算法優劣的指標就是缺頁率,而FIFO演算法的一個顯著的缺點是,在某些特定的時刻,缺頁率反而會隨著分配頁面的增加而增加,這稱為Belady現象。產生Belady現象現象的原因是,FIFO置換演算法與程序訪問記憶體的動態特徵是不相容的,被置換的記憶體頁面往往是被頻繁訪問的,或者沒有給程序分配足夠的頁面,因此FIFO演算法會使一些頁面頻繁地被替換和重新申請記憶體,從而導致缺頁率增加。因此,現在不再使用FIFO演算法。
LRU演算法#
LRU(The Least Recently Used,最近最久未使用演算法)是一種常見的快取演算法,在很多分散式快取系統(如Redis, Memcached)中都有廣泛使用。
LRU演算法的思想是:如果一個數據在最近一段時間沒有被訪問到,那麼可以認為在將來它被訪問的可能性也很小。因此,當空間滿時,最久沒有訪問的資料最先被置換(淘汰)。
LRU演算法的描述: 設計一種快取結構,該結構在構造時確定大小,假設大小為 K,並有兩個功能:
- set(key,value):將記錄(key,value)插入該結構。當快取滿時,將最久未使用的資料置換掉。
- get(key):返回key對應的value值。
實現:最樸素的思想就是用陣列+時間戳的方式,不過這樣做效率較低。因此,我們可以用雙向連結串列(LinkedList)+雜湊表(HashMap)實現(連結串列用來表示位置,雜湊表用來儲存和查詢),在Java裡有對應的資料結構LinkedHashMap。
LInkedHashMap#
利用Java
的LinkedHashMap
用非常簡單的程式碼來實現基於LRU演算法的Cache功能
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 簡單用LinkedHashMap來實現的LRU演算法的快取
*/
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int cacheSize;
public LRUCache(int cacheSize) {
super(16, (float) 0.75, true);
this.cacheSize = cacheSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
}
測試:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LRUCacheTest {
private static final Logger log = LoggerFactory.getLogger(LRUCacheTest.class);
private static LRUCache<String, Integer> cache = new LRUCache<>(10);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
cache.put("k" + i, i);
}
log.info("all cache :'{}'",cache);
cache.get("k3");
log.info("get k3 :'{}'", cache);
cache.get("k4");
log.info("get k4 :'{}'", cache);
cache.get("k4");
log.info("get k4 :'{}'", cache);
cache.put("k" + 10, 10);
log.info("After running the LRU algorithm cache :'{}'", cache);
}
}
Output:
all cache :'{k0=0, k1=1, k2=2, k3=3, k4=4, k5=5, k6=6, k7=7, k8=8, k9=9}'
get k3 :'{k0=0, k1=1, k2=2, k4=4, k5=5, k6=6, k7=7, k8=8, k9=9, k3=3}'
get k4 :'{k0=0, k1=1, k2=2, k5=5, k6=6, k7=7, k8=8, k9=9, k3=3, k4=4}'
get k4 :'{k0=0, k1=1, k2=2, k5=5, k6=6, k7=7, k8=8, k9=9, k3=3, k4=4}'
After running the LRU algorithm cache :'{k1=1, k2=2, k5=5, k6=6, k7=7, k8=8, k9=9, k3=3, k4=4, k10=10}'
LFU演算法#
LFU(Least Frequently Used ,最近最少使用演算法)也是一種常見的快取演算法。
顧名思義,LFU演算法的思想是:如果一個數據在最近一段時間很少被訪問到,那麼可以認為在將來它被訪問的可能性也很小。因此,當空間滿時,最小頻率訪問的資料最先被淘汰。
LFU 演算法的描述:
設計一種快取結構,該結構在構造時確定大小,假設大小為 K,並有兩個功能:
- set(key,value):將記錄(key,value)插入該結構。當快取滿時,將訪問頻率最低的資料置換掉。
- get(key):返回key對應的value值。
演算法實現策略:考慮到 LFU 會淘汰訪問頻率最小的資料,我們需要一種合適的方法按大小順序維護資料訪問的頻率。LFU 演算法本質上可以看做是一個 top K 問題(K = 1),即選出頻率最小的元素,因此我們很容易想到可以用二項堆來選擇頻率最小的元素,這樣的實現比較高效。最終實現策略為小頂堆+雜湊表。