redis之過期策略及記憶體淘汰機制
朝生暮死-過期策略
設定了有效期的key到期了怎麼刪除呢?
Redis會將每個設定了過期時間的key放入一個獨立的字典中,以後會定時遍歷這個字典來刪除到期的key。除了定時遍歷之外還會使用惰性刪除
過期的key。所謂惰性刪除就是在客戶端訪問這個key的時候,Redis對key的過期時間進行檢查,如果過期了就會立即刪除。所以過期key的刪除策略
是 定時刪除+惰性刪除
定時刪除:Redis預設每秒進行10次過期掃描,過期掃描不會遍歷過期字典中所有的key,而是採用了一種簡單的貪心策略,步驟如下:
1、從過期字典中隨機選出20個key
2、刪除這20個key中已經過期的key
3、如果過期的key的比例超過1/4,那就重複步驟1
同時,為了保證過期掃描不會出現迴圈過度,導致執行緒卡死的現象,演算法還增加了掃描時間的上限,預設不會超過25ms。
假設一個大型的Redis例項中所有的key在同一時間過期了,會出現怎麼樣的結果呢?毫無疑問,Redis會持續迴圈多次掃描過期字典,直到過期
字典中過期的key變得稀疏,才會停止(迴圈次數明顯下降)。這就會導致線上讀寫請求出現明顯的卡頓現象。導致這種卡頓的另外一種原因是記憶體
管理器需要頻繁回收記憶體頁,這也會產生一定的CPU消耗。
如當客戶端到來時,伺服器正好進入過期掃描狀態,客戶端的請求將會等待至少25ms後才會進行處理,如果客戶端將超時時間設定的比較短,如10
ms,那麼就會出現大量的請求因為超時而關閉。業務端會出現很多異常,而且這是你還無法從Redis的slowlog中看到慢查詢記錄,因為慢查詢指的是
邏輯處理過程慢,而不包含等待時間。所以當客戶端出現大量超時而慢查詢日誌無記錄時,可能是當前時間段大量的key過期導致的。
所以在開發過程中一定要避免在同一時間內出現大量的key同時過期。儘量給key的過期時間設定一個隨機範圍,使其過期時間均勻分佈。
從節點不會進行過期掃描,過期的處理是被動的,主節點在key到期時,會在AOF日誌檔案中增加一條del指令,同步到所有的從節點,從節點通過執行
這條del指令來刪除過期的key。因為指令同步是非同步的,所以會出現從節點的key刪除不及時的情況。
惰性刪除:
實際上Redis內部並不是只有一個主執行緒,它還有幾個非同步執行緒來處理一些耗時的操作。如果被刪除的key是一個非常大的物件,那麼del指令刪除操作就
會導致單執行緒卡頓。所以4.0版本引入了unlink指令,可以對刪除操作進行懶處理,丟給後臺執行緒來非同步回收記憶體。
在獲取某個key的時候,Redis會檢查一下這個key是否設定了過期時間以及這個是否到期了,如果到期了就交給後臺執行緒去刪除這個key,然後主執行緒什
麼也不會返回。
優勝劣汰-LRU記憶體淘汰機制
當Redis記憶體超過實體記憶體限制時,記憶體的資料會開始和磁碟產生頻繁的交換swap,交換會讓Redis的效能急劇下降,對於訪問量比較大的Redis來說,會
導致響應時間過長。所以在生產環境中不允許有這種交換行為,為了限制最大記憶體,Redis提供了配置引數maxmemory引數來限制記憶體使用閥值,當超出這個
閥值時,Redis提供了幾種可選的記憶體淘汰策略供使用者選擇以騰出空間以繼續提供讀寫服務。
1、noeviction 不會繼續服務寫請求,del和讀服務可以繼續進行,這是預設的淘汰策略
2、volatile-lru 嘗試淘汰設定了過期時間的最近最少使用的key
3、volatile-ttl 嘗試淘汰了設定了過期時間的ttl(Time to live)最少的key
4、volatile-random 嘗試從設定了過期時間的key中隨機淘汰一部分key
5、allkeys-lru 嘗試淘汰所有的key中最近最少使用的key
6、allkeys-random 嘗試從所有的key中隨機淘汰一部分key
LRU演算法
實現LRU演算法除了需要key/value字典外,還需要附加一個連結串列,連結串列中的元素按照一定的順序進行排列。當字典中的某個元素被訪問時,會將它從在鏈
表中的某個位置移動到連結串列頭部;當空間滿的時候,會踢掉連結串列尾部的元素。所以連結串列元素的排列順序就是元素最近被訪問的順序。
Redis使用的是近似的LRU演算法,因為LRU演算法需要佔用大量的額外記憶體,還需要對現有的資料結構進行比較大的改造。近似LRU演算法很簡單,在現有的資料
結構的基礎上使用隨機取樣法淘汰元素,通過給每個key增加一個額外的24bit的小欄位儲存最後一次被訪問的時間戳。而且採用的是惰性策略,Redis在執行
寫操作時,發現記憶體超過maxmemory,就會執行一次近似LRU演算法,隨機取樣出5(可以設定)個key,然後淘汰掉最舊的key,如果淘汰後記憶體仍大於
maxmemory,繼續取樣淘汰,直到記憶體小於maxmemory為止。
手寫一個LRU演算法,有三種方案
1、陣列,用陣列來儲存資料,並給每個資料項標記一個時間戳,每次插入新資料項的時候,先把陣列中存在的資料項對應的時間戳自增,並將新
資料項的時間戳置為0並插入到陣列中。每次訪問陣列中新資料項的時候,將被訪問的資料項的時間戳置為0。當陣列空間滿時,將時間戳最大的數
據項淘汰。
2、連結串列,每次插入新資料的時候將新資料插入到連結串列的頭部,每次訪問資料也將被訪問的資料移動到連結串列頭部,當連結串列滿時將連結串列尾部的資料淘汰
3、連結串列+hashMap,LinkedHashMap。當需要插入新的資料項的時候,如果新資料項在連結串列中存在(即命中),則把該節點移動到連結串列頭部,如
果不存在,則新建一個節點,放到連結串列頭部,若快取滿了,則把連結串列最後一個節點刪除即可。在訪問資料的時候,如果資料項在連結串列中存在,則把
該節點移到連結串列頭部,否則返回-1,這樣連結串列尾部的節點就是最近最少訪問的資料項。
分析:使用陣列需要不停維護資料項的訪問時間戳,並且在插入資料,訪問和刪除資料(不知道陣列下標)的時候,時間複雜度都是O(n),僅使用連結串列的情況下,
在訪問定位資料的時間複雜度為O(n),所以一般使用LinkedHashMap的方式。LinkedHashMap的底層就是使用HashMap加雙向連結串列實現的,而且本身是有序的(
插入和訪問順序相同),新插入的元素放入連結串列的尾部;且其有removeEldestEntry方法用於移除最老的元素,不過預設返回false,表示不移除,需要重寫此方法當超過map容量時移除最老的元素即可。
LinkedHashMap:
/** * Returns <tt>true</tt> if this map should remove its eldest entry. * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after * inserting a new entry into the map. It provides the implementor * with the opportunity to remove the eldest entry each time a new one * is added. This is useful if the map represents a cache: it allows * the map to reduce memory consumption by deleting stale entries. * * <p>Sample use: this override will allow the map to grow up to 100 * entries and then delete the eldest entry each time a new entry is * added, maintaining a steady state of 100 entries. * <pre> * private static final int MAX_ENTRIES = 100; * * protected boolean removeEldestEntry(Map.Entry eldest) { * return size() > MAX_ENTRIES; * } * </pre> * * <p>This method typically does not modify the map in any way, * instead allowing the map to modify itself as directed by its * return value. It <i>is</i> permitted for this method to modify * the map directly, but if it does so, it <i>must</i> return * <tt>false</tt> (indicating that the map should not attempt any * further modification). The effects of returning <tt>true</tt> * after modifying the map from within this method are unspecified. * * <p>This implementation merely returns <tt>false</tt> (so that this * map acts like a normal map - the eldest element is never removed). * * @param eldest The least recently inserted entry in the map, or if * this is an access-ordered map, the least recently accessed * entry. This is the entry that will be removed it this * method returns <tt>true</tt>. If the map was empty prior * to the <tt>put</tt> or <tt>putAll</tt> invocation resulting * in this invocation, this will be the entry that was just * inserted; in other words, if the map contains a single * entry, the eldest entry is also the newest. * @return <tt>true</tt> if the eldest entry should be removed * from the map; <tt>false</tt> if it should be retained. */ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
構造方法:
/** * Constructs an empty <tt>LinkedHashMap</tt> instance with the * specified initial capacity, load factor and ordering mode. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @param accessOrder the ordering mode - <tt>true</tt> for * access-order, <tt>false</tt> for insertion-order * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
自定義LRU實現:
package lru; import java.util.LinkedHashMap; import java.util.Map; /** * LinkedHashMap 實現LRU演算法 * 〈功能詳細描述〉 * * @author 17090889 * @see [相關類/方法](可選) * @since [產品/模組版本] (可選) */ public class LRU<k, v> extends LinkedHashMap<k, v> { /** * 容量,實際能儲存多少資料 */ private final int MAX_ENTRIES; /** * Math.ceil(cacheSize/0.75f)+1 HashMap的initialCapacity * 0.75f 負載因子 * accessOrder 排序模式,true:按照訪問順序進行排序,最近訪問的放在尾部 false:按照插入順序 * * @param maxEntries */ public LRU(int maxEntries) { super((int) (Math.ceil(maxEntries / 0.75f) + 1), 0.75f, true); MAX_ENTRIES = maxEntries; } /** * 重寫移除最老元素方法 * 返回true,表示刪除最老元素 false 表示不刪除 * * @param eldest * @return */ @Override protected boolean removeEldestEntry(Map.Entry<k, v> eldest) { // 當實際容量大於指定的容量的時候就自動刪除最老的元素,連結串列頭部的元素 return size() > MAX_ENTRIES; } }
插入測試:
LRU<String, String> lru = new LRU<>(5); lru.put("3", "3"); lru.put("1", "1"); lru.put("5", "5"); lru.put("2", "22"); lru.put("3", "33"); lru.get("5");
遍歷得到的結果:1 2 3 5
儲存連結串列結構為:
LFU演算法
Redis 4.0 引入了一個新的淘汰策略,LFU(Lasted Fequently Used):最少頻繁使用。按照最近的訪問頻率進行淘汰,比LRU更加精確地表示了一個key被訪問的熱度。
如果一個key長時間不被訪問,只是偶爾被訪問了一下,那麼它在LRU演算法中就被移動到了連結串列的頭部,是不容易被淘汰的,因為LRU演算法會認為它是一個熱點key。
而LFU需要追蹤最近一段時間內key的訪問頻率,只有最近一段時間內被訪問多次的key,LFU才認為是熱點key。
END.