FIFO/LRU/LFU三種快取演算法

小姐姐味道發表於2019-07-09

FIFO/LRU/LFU三種快取演算法

更多精彩文章。

《微服務不是全部,只是特定領域的子集》

《“分庫分表" ?選型和流程要慎重,否則會失控》

這麼多監控元件,總有一款適合你

《Linux生產環境上,最常用的一套“vim“技巧》

《使用Netty,我們到底在開發些什麼?》

最有用系列:

《Linux生產環境上,最常用的一套“vim“技巧》

《Linux生產環境上,最常用的一套“Sed“技巧》

《Linux生產環境上,最常用的一套“AWK“技巧》

歡迎Linux和java後端的同學關注公眾號。

JVM內快取是快取體系中重要的一環,最常用的有FIFO/LRU/LFU三種演算法。

1、FIFO是簡單的佇列,先進先出。
2、LRU是最近最少使用,優先移除最久未使用的資料。是時間維度
3、LFU是最近最不常用,優先移除訪問次數最少的資料。是統計維度

由於過期也是快取的一個重要特點。所有在設計這三種快取演算法時,需要額外的儲存空間去儲存這個過期時間。

以下將討論這三種快取演算法的操作和設計要點,暫未考慮高併發環境。

FIFO

先進先出,如果快取容量滿,則優先移出最早加入快取的資料;其內部可以使用佇列實現。

FIFO/LRU/LFU三種快取演算法

特點

1)Object get(key):獲取儲存的資料,如果資料不存在或者已經過期,則返回null。
2)void put(key,value,expireTime):加入快取,無論此key是否已存在,均作為新key處理(移除舊key);如果空間不足,則移除已過期的key,如果沒有,則移除最早加入快取的key。過期時間未指定,則表示永不自動過期。
3)此題需要注意,我們允許key是有過期時間的,這一點與普通的FIFO有所區別,所以在設計此題時需要注意。(也是面試考察點,此題偏設計而非演算法)

普通的FIFO或許大家都能很簡單的寫出,此處增加了過期時間的特性,所以在設計時需要多考慮。如下示例,為FIFO的簡易設計,尚未考慮併發環境場景。

設計思路

1)用普通的hashMap儲存快取資料。
2)我們需要額外的map用來儲存key的過期特性,例子中使用了TreeMap,將“剩餘存活時間”作為key,利用treemap的排序特性。

public class FIFOCache {  
  
    //按照訪問時間排序,儲存所有key-value  
    private final Map<String,Value> CACHE = new LinkedHashMap<>();  
  
    //過期資料,只儲存有過期時間的key  
    //暫不考慮併發,我們認為同一個時間內沒有重複的key,如果改造的話,可以將value換成set  
    private final TreeMap<Long, String> EXPIRED = new TreeMap<>();  
  
    private final int capacity;  
  
    public FIFOCache(int capacity) {  
        this.capacity = capacity;  
    }  
  
    public Object get(String key) {  
        //  
        Value value = CACHE.get(key);  
        if (value == null) {  
            return null;  
        }  
  
        //如果不包含過期時間  
        long expired = value.expired;  
        long now = System.nanoTime();  
        //已過期  
        if (expired > 0 && expired <= now) {  
            CACHE.remove(key);  
            EXPIRED.remove(expired);  
            return null;  
        }  
        return value.value;  
    }  
  
    public void put(String key,Object value) {  
        put(key,value,-1);  
    }  
  
  
    public void put(String key,Object value,int seconds) {  
        //如果容量不足,移除過期資料  
        if (capacity < CACHE.size()) {  
            long now = System.nanoTime();  
            //有過期的,全部移除  
            Iterator<Long> iterator = EXPIRED.keySet().iterator();  
            while (iterator.hasNext()) {  
                long _key = iterator.next();  
                //如果已過期,或者容量仍然溢位,則刪除  
                if (_key > now) {  
                    break;  
                }  
                //一次移除所有過期key  
                String _value = EXPIRED.get(_key);  
                CACHE.remove(_value);  
                iterator.remove();  
            }  
        }  
  
        //如果仍然容量不足,則移除最早訪問的資料  
        if (capacity < CACHE.size()) {  
            Iterator<String> iterator = CACHE.keySet().iterator();  
            while (iterator.hasNext() && capacity < CACHE.size()) {  
                String _key = iterator.next();  
                Value _value = CACHE.get(_key);  
                long expired = _value.expired;  
                if (expired > 0) {  
                    EXPIRED.remove(expired);  
                }  
                iterator.remove();  
            }  
        }  
  
        //如果此key已存在,移除舊資料  
        Value current = CACHE.remove(key);  
        if (current != null && current.expired > 0) {  
            EXPIRED.remove(current.expired);  
        }  
        //如果指定了過期時間  
        if(seconds > 0) {  
            long expireTime = expiredTime(seconds);  
            EXPIRED.put(expireTime,key);  
            CACHE.put(key,new Value(expireTime,value));  
        } else {  
            CACHE.put(key,new Value(-1,value));  
        }  
  
    }  
  
    private long expiredTime(int expired) {  
        return System.nanoTime() + TimeUnit.SECONDS.toNanos(expired);  
    }  
  
    public void remove(String key) {  
        Value value = CACHE.remove(key);  
        if(value == null) {  
            return;  
        }  
        long expired = value.expired;  
        if (expired > 0) {  
            EXPIRED.remove(expired);  
        }  
    }  
  
  
    class Value {  
        long expired; //過期時間,納秒  
        Object value;  
        Value(long expired,Object value) {  
            this.expired = expired;  
            this.value = value;  
        }  
    }  
}  
複製程式碼

LRU

least recently used,最近最少使用,是目前最常用的快取演算法和設計方案之一,其移除策略為“當快取(頁)滿時,優先移除最近最久未使用的資料”,優點是易於設計和使用,適用場景廣泛。演算法可以參考leetcode 146 (LRU Cache)。

特點

1)Object get(key):從canche中獲取key對應的資料,如果此key已過期,移除此key,並則返回null。
2)void put(key,value,expired):設定k-v,如果容量不足,則根據LRU置換演算法移除“最久未被使用的key”,需要注意,根據LRU優先移除已過期的keys,如果沒有,則根據LRU移除未過期的key。如果未設定過期時間,則認為永不自動過期。
3)此題,設計關鍵是過期時間特性,這與常規的LRU有所不同。畢竟“過期時間”特性在cache設計中是必要的。

設計思路

1)LRU的基礎演算法,需要了解;每次put、get時需要更新key對應的訪問時間,我們需要一個資料結構能夠儲存key最近的訪問時間且能夠排序。
2)既然包含過期時間特性,那麼帶有過期時間的key需要額外的資料結構儲存。
3)暫時不考慮併發操作;儘量兼顧空間複雜度和時間複雜度。
4)此題仍然偏向於設計題,而非純粹的演算法題。
此題程式碼與FIFO基本相同,唯一不同點為get()方法,對於LRU而言,get方法需要重設訪問時間(即調整所在cache中順序)

public Object get(String key) {  
    //  
    Value value = CACHE.get(key);  
    if (value == null) {  
        return null;  
    }  
  
    //如果不包含過期時間  
    long expired = value.expired;  
    long now = System.nanoTime();  
    //已過期  
    if (expired > 0 && expired <= now) {  
        CACHE.remove(key);  
        EXPIRED.remove(expired);  
        return null;  
    }  
    //相對於FIFO,增加順序重置  
    CACHE.remove(key);  
    CACHE.put(key,value);  
    return value.value;  
}  
複製程式碼

LFU

最近最不常用,當快取容量滿時,移除訪問次數最少的元素,如果訪問次數相同的元素有多個,則移除最久訪問的那個。設計要求參見leetcode 460( LFU Cache)

public class LFUCache {  
  
    //主要容器,用於儲存k-v  
    private Map<String, Object> keyToValue = new HashMap<>();  
  
    //記錄每個k被訪問的次數  
    private Map<String, Integer> keyToCount = new HashMap<>();  
  
    //訪問相同次數的key列表,按照訪問次數排序,value為相同訪問次數到key列表。  
    private TreeMap<Integer, LinkedHashSet<String>> countToLRUKeys = new TreeMap<>();  
  
    private int capacity;  
  
    public LFUCache(int capacity) {  
        this.capacity = capacity;  
        //初始化,預設訪問1次,主要是解決下文  
    }  
  
    public Object get(String key) {  
        if (!keyToValue.containsKey(key)) {  
            return null;  
        }  
  
        touch(key);  
        return keyToValue.get(key);  
    }  
  
    /** 
     * 如果一個key被訪問,應該將其訪問次數調整。 
     * @param key 
     */  
    private void touch(String key) {  
        int count = keyToCount.get(key);  
        keyToCount.put(key, count + 1);//訪問次數增加  
        //從原有訪問次數統計列表中移除  
        countToLRUKeys.get(count).remove(key);  
  
        //如果符合最少呼叫次數到key統計列表為空,則移除此呼叫次數到統計  
        if (countToLRUKeys.get(count).size() == 0) {  
            countToLRUKeys.remove(count);  
        }  
  
        //然後將此key的統計資訊加入到管理列表中  
        LinkedHashSet<String> countKeys = countToLRUKeys.get(count + 1);  
        if (countKeys == null) {  
            countKeys = new LinkedHashSet<>();  
            countToLRUKeys.put(count + 1,countKeys);  
        }  
        countKeys.add(key);  
    }  
  
    public void put(String key, Object value) {  
        if (capacity <= 0) {  
            return;  
        }  
  
        if (keyToValue.containsKey(key)) {  
            keyToValue.put(key, value);  
            touch(key);  
            return;  
        }  
        //容量超額之後,移除訪問次數最少的元素  
        if (keyToValue.size() >= capacity) {  
            Map.Entry<Integer,LinkedHashSet<String>> entry = countToLRUKeys.firstEntry();  
            Iterator<String> it = entry.getValue().iterator();  
            String evictKey = it.next();  
            it.remove();  
            if (!it.hasNext()) {  
                countToLRUKeys.remove(entry.getKey());  
            }  
            keyToCount.remove(evictKey);  
            keyToValue.remove(evictKey);  
  
        }  
  
        keyToValue.put(key, value);  
        keyToCount.put(key, 1);  
        LinkedHashSet<String> keys = countToLRUKeys.get(1);  
        if (keys == null) {  
            keys = new LinkedHashSet<>();  
            countToLRUKeys.put(1,keys);  
        }  
        keys.add(key);  
    }  
}  
複製程式碼

End

更加易用的cache,可以參考guava的實現。希望這三個程式碼模版,能夠對你有所幫助。

FIFO/LRU/LFU三種快取演算法

相關文章