Redis(八)—— LRU Cachegit
在計算機中緩存可謂無所不在,不管仍是應用仍是操做系統中,爲了性能都須要作緩存。然緩存必然與緩存算法息息相關,LRU就是其中之一。筆者在最早接觸LRU是大學學習操做系統時的瞭解到的,至今已經很是模糊。在學習Redis時,又再次與其相遇,這裏將這塊內容好好梳理總結。github
LRU(Least Recently Used)是緩存算法家族的一員——最近最少使用算法,相似的算法還有FIFO(先進先出)、LIFO(後進先出)等。由於緩存的選擇通常都是用內存(RAM)或者計算機中的L1甚至L2高速緩存,無疑這些存儲器都是有大小限制,使用的代價都很是高昂,不可能將全部須要緩存的數據都緩存起來,須要使用特定的算法,好比LRU將不經常使用的數據驅逐出緩存,以騰出更多的空間存儲更有價值的數據。redis
上圖引用wiki上描述LRU算法的圖。其中A是最近最少被使用,因此當緩存E時,根據LRU的特徵,驅逐A,存儲E。算法
在LRU中最核心的部分是"最近最少",因此必然須要對緩存數據的訪問(讀和寫)作跟蹤記錄——即何時使用了什麼數據!數據庫
本文先介紹Redis中的LRU特色,再介紹如何自實現一個LRU。瀏覽器
前文中介紹Redis是基於內存的K-V數據結構服務器。Redis在存儲方面有內存容量限制。在Redis中經過maxmemory配置設置Redis可使用的最大內存。如:緩存
maxmemory 100mb
以上配置則限制Redis可使用的最大內存爲100M。當設置爲0時,表示沒有進行內存限制。安全
上述說到Redis的內存限制問題,若是Redis使用的內存達到限制,再進行寫入操做,Redis該如何處理?
Redis中有不少驅逐策略——Eviction Policies,當內存達到限制時,Redis會使用其配置的策略對內存中的數據進行驅逐回收,釋放內存空間,以便存儲待寫入的數據。服務器
Redis中有如下的內存策略可供選擇數據結構
其中volatile-lru、volatile-random和volatile-ttl策略驅逐,若是沒有可驅逐的鍵,則結果和noeviction策略同樣,都是返回錯誤。
Notes:
在Redis中的LRU算法並非徹底精確的LRU實現,只是近似的LRU算法——即經過採樣一些鍵,而後從採樣中按照LRU驅逐。若是要實現徹底精確的LRU算法,勢必須要跨越整個Redis內存進行統計,這樣對性能就有折扣。在性能和LRU之間的trade off。
關於更多詳細內容,請參考:Approximated LRU algorithm
上述介紹了Redis中對於內存限制實現其LRU策略,下面筆者綜合平時所學,簡單實現一個LRU Cache,加深對其理解。
實現LRU Cache的關鍵點在於:
對於第一點,可以實現快速查找的數據結構Map是必選,映射表結構具備自然的快速查找的特色。
對於第二點,要麼對每一個元素維護一份訪問信息——每一個元素都有一個訪問時間字段,要麼以特定的順序表示其訪問信息——列表先後順序表示訪問時間排序。
基於以上分析,JDK中提供了Map和訪問順序的數據結構——LinkedHashMap。這裏爲了簡單,基於LinkedHashMap實現。固然感興趣還能夠基於Map接口自實現,不過都是大同小異。
固然還可使用第二點中的第一種方式,可是這樣須要跨越整個緩存空間,遍歷比較每一個元素的訪問時間,代價高昂。
先定義LRUCache的接口:
public interface LRUCache<K, V> { V put(K key, V value); V get(K key); int getCapacity(); int getCurrentSize(); }
而後繼承實現LinkedHashMap,在LinkedHashMap中有布爾accessOrder屬性控制其順序:true表示按照訪問順序,false表示按照插入順序。 在構造LinkedHashMap時須要設置true,表示按照訪問順序進行迭代。
重寫removeEldestEntry方法:若是當前緩存中的元素個數大於緩存的容量,則返回true,表示須要移除元素。
/** * 基於{@link LinkedHashMap}實現的LRU Cache,該緩存是非線程安全,須要caller保證同步。 * 原理: * 1.LinkedHashMap中使用雙向循環鏈表的順序有兩種,其中訪問順序表示最近最少未被訪問的順序 * 2.基於HashMap,因此get的算法複雜度O(1) * * @author huaijin */ public final class LRUCacheBaseLinkedHashMap<K, V> extends LinkedHashMap<K, V> implements LRUCache<K, V>{ private static final int DEFAULT_CAPACITY = 16; private static final float DEFAULT_LOAD_FACTOR = 0.75F; /** * 緩存大小 */ private int capacity; public LRUCacheBaseLinkedHashMap() { this(DEFAULT_CAPACITY); } public LRUCacheBaseLinkedHashMap(int capacity) { this(capacity, DEFAULT_LOAD_FACTOR); } public LRUCacheBaseLinkedHashMap(int capacity, float loadFactor) { super(capacity, loadFactor, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } @Override public V put(K key, V value) { Objects.requireNonNull(key, "key must not be null."); Objects.requireNonNull(value, "value must not be null."); return super.put(key, value); } @Override public String toString() { List<String> sb = new ArrayList<>(); Set<Map.Entry<K, V>> entries = entrySet(); for (Map.Entry<K, V> entry : entries) { sb.add(entry.getKey().toString() + ":" + entry.getValue().toString()); } return String.join(" ", sb); } @Override public int getCapacity() { return capacity; } @Override public int getCurrentSize() { return size(); } }
若是對LinkedHashMap不是很熟悉,請移步至Map 綜述(二):徹頭徹尾理解 LinkedHashMap
對於使用Cache而言,Guava工具庫中的Guava Cache是一個很是不錯的選擇。其優點在於:
詳細狀況能夠參考:Caches。在Guava Cache中的驅逐策略有基於大小的策略,該策略就是LRU的實現:
Cache<String, String> guavaCache = CacheBuilder.newBuilder() .maximumSize(5) .build();
當Cache的容量達到5個時,若是再往緩存中寫入數據,Cache將淘汰最近最少被使用的數據。
測試以下案例以下:
@Test public void testLRUGuavaCacheBaseSize() throws ExecutionException { Cache<String, String> guavaCache = CacheBuilder.newBuilder() .maximumSize(5) .build(); guavaCache.put("1", "1"); guavaCache.put("2", "2"); guavaCache.put("3", "3"); guavaCache.put("4", "4"); guavaCache.put("5", "5"); printGuavaCache("原cache:", guavaCache); guavaCache.getIfPresent("1"); guavaCache.put("6", "6"); printGuavaCache("put一次後cache:", guavaCache); }
執行結果:
原cache:2:2 3:3 1:1 5:5 4:4 put一次後cache:6:6 3:3 1:1 5:5 4:4
由於數據1被get一次,致使2是最近最少被使用,當put 6時,將2淘汰驅逐。
緩存目的就是爲了提升訪問速度以帶來性能上質的提高。可是緩存的容量和命中率倒是從反比。 基於內存存儲數據,不管應用本地緩存,仍是分佈式緩存服務器甚至操做系統,都須要考慮存儲容量的限制對命中率的影響。採用合適緩存算法,對提升緩存命中率至爲關鍵。