徹底弄懂Redis的內存淘汰策略


Redis的數據已經設置了TTL,不是過期就已經刪除了嗎?為什么還存在所謂的淘汰策略呢?這個原因我們需要從redis的過期策略聊起。

過期策略

定期刪除

redis 會將每個設置了過期時間的 key 放入到一個獨立的字典中,以后會定期遍歷這個字典來刪除到期的 key。

Redis 默認會每秒進行十次過期掃描(100ms一次),過期掃描不會遍歷過期字典中所有的 key,而是采用了一種簡單的貪心策略。

1.從過期字典中隨機 20 個 key;

2.刪除這 20 個 key 中已經過期的 key;

3.如果過期的 key 比率超過 1/4,那就重復步驟 1;

redis默認是每隔 100ms就隨機抽取一些設置了過期時間的key,檢查其是否過期,如果過期就刪除。注意這里是隨機抽取的。為什么要隨機呢?你想一想假如 redis 存了幾十萬個 key ,每隔100ms就遍歷所有的設置過期時間的 key 的話,就會給 CPU 帶來很大的負載。

惰性刪除

所謂惰性策略就是在客戶端訪問這個key的時候,redis對key的過期時間進行檢查,如果過期了就立即刪除,不會給你返回任何東西。

定期刪除可能會導致很多過期key到了時間並沒有被刪除掉。所以就有了惰性刪除。假如你的過期 key,靠定期刪除沒有被刪除掉,還停留在內存里,除非你的系統去查一下那個 key,才會被redis給刪除掉。這就是所謂的惰性刪除,即當你主動去查過期的key時,如果發現key過期了,就立即進行刪除,不返回任何東西.

總結:定期刪除是集中處理,惰性刪除是零散處理。

為什么需要淘汰策略

有了以上過期策略的說明后,就很容易理解為什么需要淘汰策略了,因為不管是定期采樣刪除還是惰性刪除都不是一種完全精准的刪除,就還是會存在key沒有被刪除掉的場景,所以就需要內存淘汰策略進行補充。

內存淘汰策略

1. noeviction:當內存使用超過配置的時候會返回錯誤,不會驅逐任何鍵

2. allkeys-lru:加入鍵的時候,如果過限,首先通過LRU算法驅逐最久沒有使用的鍵

3. volatile-lru:加入鍵的時候如果過限,首先從設置了過期時間的鍵集合中驅逐最久沒有使用的鍵

4. allkeys-random:加入鍵的時候如果過限,從所有key隨機刪除

5. volatile-random:加入鍵的時候如果過限,從過期鍵的集合中隨機驅逐

6. volatile-ttl:從配置了過期時間的鍵中驅逐馬上就要過期的鍵

7. volatile-lfu:從所有配置了過期時間的鍵中驅逐使用頻率最少的鍵

8. allkeys-lfu:從所有鍵中驅逐使用頻率最少的鍵

LRU

標准LRU實現方式

1. 新增key value的時候首先在鏈表結尾添加Node節點,如果超過LRU設置的閾值就淘汰隊頭的節點並刪除掉HashMap中對應的節點。

2. 修改key對應的值的時候先修改對應的Node中的值,然后把Node節點移動隊尾。

3. 訪問key對應的值的時候把訪問的Node節點移動到隊尾即可。

Redis的LRU實現

Redis維護了一個24位時鍾,可以簡單理解為當前系統的時間戳,每隔一定時間會更新這個時鍾。每個key對象內部同樣維護了一個24位的時鍾,當新增key對象的時候會把系統的時鍾賦值到這個內部對象時鍾。比如我現在要進行LRU,那么首先拿到當前的全局時鍾,然后再找到內部時鍾與全局時鍾距離時間最久的(差最大)進行淘汰,這里值得注意的是全局時鍾只有24位,按秒為單位來表示才能存儲194天,所以可能會出現key的時鍾大於全局時鍾的情況,如果這種情況出現那么就兩個相加而不是相減來求最久的key。

struct redisServer {
       pid_t pid; 
       char *configfile; 
       //全局時鍾        unsigned lruclock:LRU_BITS; 
       ...
};
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    /* key對象內部時鍾 */
    unsigned lru:LRU_BITS;
    int refcount;
    void *ptr;
} robj;

Redis中的LRU與常規的LRU實現並不相同,常規LRU會准確的淘汰掉隊頭的元素,但是Redis的LRU並不維護隊列,只是根據配置的策略要么從所有的key中隨機選擇N個(N可以配置)要么從所有的設置了過期時間的key中選出N個鍵,然后再從這N個鍵中選出最久沒有使用的一個key進行淘汰。

下圖是常規LRU淘汰策略與Redis隨機樣本取一鍵淘汰策略的對比,淺灰色表示已經刪除的鍵,深灰色表示沒有被刪除的鍵,綠色表示新加入的鍵,越往上表示鍵加入的時間越久。從圖中可以看出,在redis 3中,設置樣本數為10的時候能夠很准確的淘汰掉最久沒有使用的鍵,與常規LRU基本持平。

為什么要使用近似LRU?

1、性能問題,由於近似LRU算法只是最多隨機采樣N個key並對其進行排序,如果精准需要對所有key進行排序,這樣近似LRU性能更高

2、內存占用問題,redis對內存要求很高,會盡量降低內存使用率,如果是抽樣排序可以有效降低內存的占用

3、實際效果基本相等,如果請求符合長尾法則,那么真實LRU與Redis LRU之間表現基本無差異

4、在近似情況下提供可自配置的取樣率來提升精准度,例如通過 CONFIG SET maxmemory-samples <count> 指令可以設置取樣數,取樣數越高越精准,如果你的CPU和內存有足夠,可以提高取樣數看命中率來探測最佳的采樣比例。

LFU

LFU是在Redis4.0后出現的,LRU的最近最少使用實際上並不精確,考慮下面的情況,如果在|處刪除,那么A距離的時間最久,但實際上A的使用頻率要比B頻繁,所以合理的淘汰策略應該是淘汰B。LFU就是為應對這種情況而生的。

A~~A~~A~~A~~A~~A~~A~~A~~A~~A~~~|

B~~~~~B~~~~~B~~~~~B~~~~~~~~~~~~B|

LFU把原來的key對象的內部時鍾的24位分成兩部分,前16位還代表時鍾,后8位代表一個計數器。16位的情況下如果還按照秒為單位就會導致不夠用,所以一般這里以時鍾為單位。而后8位表示當前key對象的訪問頻率,8位只能代表255,但是redis並沒有采用線性上升的方式,而是通過一個復雜的公式,通過配置如下兩個參數來調整數據的遞增速度。

lfu-log-factor 可以調整計數器counter的增長速度,lfu-log-factor越大,counter增長的越慢。

lfu-decay-time 是一個以分鍾為單位的數值,可以調整counter的減少速度。

所以這兩個因素就對應到了LFU的Counter減少策略和增長策略,它們實現邏輯分別如下。

降低LFUDecrAndReturn

1、先從高16位獲取最近的降低時間ldt以及低8位的計數器counter值

2、計算當前時間now與ldt的差值(now-ldt),當ldt大於now時,那說明是過了一個周期,按照65535-ldt+now計算(16位一個周期最大65535)

3、使用第2步計算的差值除以lfu_decay_time,即LFUTimeElapsed(ldt) / server.lfu_decay_time,已過去n個lfu_decay_time,則將counter減少n。

增長LFULogIncr

1、獲取0-1的隨機數r

2、計算0-1之間的控制因子p,它的計算邏輯如下

//LFU_INIT_VAL默認為5 baseval = counter - LFU_INIT_VAL;
//計算控制因子 p = 1.0/(baseval*lfu_log_factor+1);

3、如果r小於p,counter增長1

p取決於當前counter值與lfu_log_factor因子,counter值與lfu_log_factor因子越大,p越小,r小於p的概率也越小,counter增長的概率也就越小。增長情況如下圖:

從左到右表示key的命中次數,從上到下表示影響因子,在影響因子為100的條件下,經過10M次命中才能把后8位值加滿到255.

新生KEY策略

另外一個問題是,當創建新對象的時候,對象的counter如果為0,很容易就會被淘汰掉,還需要為新生key設置一個初始counter。counter會被初始化為LFU_INIT_VAL,默認5。

原文鏈接:
https://stor.51cto.com/art/201904/594773.htm


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM