FIFO/LRU/LFU三種緩存算法

更多精彩文章。java

《微服務不是所有,只是特定領域的子集》面試

《「分庫分表" ?選型和流程要慎重,不然會失控》算法

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

《Linux生產環境上,最經常使用的一套「vim「技巧》後端

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

最有用系列:bash

《Linux生產環境上,最經常使用的一套「vim「技巧》數據結構

《Linux生產環境上,最經常使用的一套「Sed「技巧》併發

《Linux生產環境上,最經常使用的一套「AWK「技巧》微服務

歡迎Linux和java後端的同窗關注公衆號。

JVM內緩存是緩存體系中重要的一環,最經常使用的有FIFO/LRU/LFU三種算法。

一、FIFO是簡單的隊列,先進先出。
二、LRU是最近最少使用,優先移除最久未使用的數據。是時間維度
三、LFU是最近最不經常使用,優先移除訪問次數最少的數據。是統計維度

因爲過時也是緩存的一個重要特色。全部在設計這三種緩存算法時,須要額外的存儲空間去存儲這個過時時間。

如下將討論這三種緩存算法的操做和設計要點,暫未考慮高併發環境。

FIFO

先進先出,若是緩存容量滿,則優先移出最先加入緩存的數據;其內部可使用隊列實現。

特色

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的實現。但願這三個代碼模版,可以對你有所幫助。

相關文章
相關標籤/搜索