前言:

  我們的redis使用的是記憶體空間來儲存資料的,但是記憶體空間畢竟有限,隨著我們儲存資料的不斷增長,當超過了我們的記憶體大小時,即在redis中設定的快取大小(maxmeory 4GB),redis會怎麼處理呢?

Redis記憶體淘汰策略,是被很多小夥伴忽略的知識盲區,注意,是盲區。

注意,Redis如果記憶體淘汰策略配置不合理,可能會導致Redis無法服務。

今天就來聊聊redis的快取淘汰策略:↓ ↓ ↓

首先,介紹一下Redis過期刪除策略,然後,再介紹Redis淘汰策略.

1):Redis過期刪除策略 

Redis對於過期的key,有三種刪除策略:

  • 定期刪除(主動刪除:由於惰性刪除策略無法保證冷資料被及時刪掉,所以Redis會定期主動淘汰一批已過期的key)
  • 惰性刪除(被動刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key)
  • 當前已用記憶體超過maxmemory限定時,觸發主動清理策略

定期刪除:

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過期了,就立即進行刪除,不返回任何東西.

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

2):Redis 記憶體爆滿 淘汰置換策略 

當 Redis 記憶體使用達到 maxmemory 時,需要選擇設定好的 maxmemory-policy 進行對老資料的置換。

下面是可以選擇的置換策略:

不同於之前的版本,redis5.0為我們提供了八個不同的記憶體置換策略;很早之前提供了6種。

  1. volatile-lru:從已設定過期時間的資料集中挑選最近最少使用的資料淘汰。
  2. volatile-ttl:從已設定過期時間的資料集中挑選將要過期的資料淘汰。
  3. volatile-random:從已設定過期時間的資料集中任意選擇資料淘汰。
  4. volatile-lfu:從已設定過期時間的資料集挑選使用頻率最低的資料淘汰。
  5. allkeys-lru:從資料集中挑選最近最少使用的資料淘汰
  6. allkeys-lfu:從資料集中挑選使用頻率最低的資料淘汰。
  7. allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰
  8. no-enviction(驅逐):禁止驅逐資料,這也是預設策略。

意思是當記憶體不足以容納新入資料時,新寫入操作就會報錯,請求可以繼續進行,線上任務也不能持續進行,採用no-enviction策略可以保證資料不被丟失。

這八種大體上可以分為4中:

  1. lru(Least Recently Used,最近最少使用)
  2. lfu(Least Frequently Used,最不經常使用)、
  3. random(隨機)
  4. ttl

設定 maxmemory-policy 的方法 和 設定 maxmemory 方法類似,通過 redis.conf 或是通過 CONFIG SET 動態修改。

選擇合適的置換策略是很重要的,這主要取決於你的應用的訪問模式,當然你也可以動態的修改置換策略;

並通過用 Redis 命令——INFO 去輸出 cache 的命中率情況,進而可以對置換策略進行調優。

置換策略是如何工作的?

  1. 客戶端執行一條新命令,導致資料庫需要增加資料(比如set key value)
  2. Redis會檢查記憶體使用,如果記憶體使用超過 maxmemory,就會按照置換策略刪除一些 key
  3. 新的命令執行成功

我們持續的寫資料會導致記憶體達到或超出上限 maxmemory,但是置換策略會將記憶體使用降低到上限以下。

如果一次需要使用很多的記憶體(比如一次寫入一個很大的set),那麼,Redis 的記憶體使用可能超出最大記憶體限制一段時間。

LRU 演算法機制:

LRU演算法的全稱叫做Least Recently Used,也就是最近最少使用原則來進行資料的淘汰演算法。

其原理就是,會將資料放入到一個連結串列中,當連結串列中的某個元素被訪問時,這個元素就被會提到連結串列的前面,其他元素,預設向後移動;

當這個時候我們想快取中新增一個元素時,也會被增加到連結串列的頭部,那麼尾部的最後一個元素就被淘汰了。

lru的實現思想就是:就是剛被訪問的資料,在接下來的時間裡,更容易被再次訪問,而一段時間沒有被訪問的資料,之後也不會再次訪問。

但是要將redis的全部資料都放入這樣一個連結串列中的話,redis的資料被頻繁訪問時,需要不停的移動連結串列的位置,降低redis的效能。

所以redis對LRU演算法進行了優化 ↓

在redis中,會給每個資料記錄一個最近訪問的時間戳(記錄在RedisObject的lru欄位中),

當需要進行資料淘汰時,redis就隨機篩選出N個數據放入到候選集合中去,然後比較這N個數據中的lru的值,最小的就會被淘汰。

當再次需要淘汰資料時,這個時候篩選資料放入到第一次建立的淘汰集合中,那麼這次篩選就不在是隨機篩選,而是能進入候選集合的資料的 lru 欄位值必須小於候選集合中最小的 lru 值,

然後再次將最小的lru的值的資料進行淘汰。

其中N的配置項為:

maxmemory-samples 100 # 表示N為100

LFU 演算法機制:

LFU(Least frequently used)稱為最近使用最少的資料將被淘汰,redis在就是在LRU的基礎上增加一個次數統計。

其步驟就是根據資料的訪問次數進行篩選,淘汰訪問次數少的資料,如果訪問次數相同,在根據訪問時間進行比較,淘汰訪問時間久遠的資料。

redis中的實現方式:就是在RedisObject的欄位lru上,拆分為兩個部分:

  • ldt值:lru欄位的前16bit位,還是用來表示時間戳。
  • counter值:lru欄位的後8bit位,用來表示資料的訪問次數。

當 LFU 策略篩選資料時,Redis 會在候選集合中,根據資料 lru 欄位的後 8bit 選擇訪問次數最少的資料進行淘汰。

當訪問次數相同時,再根據 lru 欄位的前 16bit 值大小,選擇訪問時間最久遠的資料進行淘汰。

但是8個bit位,最大隻能記錄255的值,但是redis中的資料,有時候會被訪問成千上萬次,那麼這個問題如何進行解決呢?

redis對計數進行了優化,並不是資料被訪問一次,counter就會被加1,而是遵循如下規則:↓

當資料被訪問一次時,首先用計數器當前的值乘以配置項lfu_log_factor再加1,再取倒數得到一個p值然後把這個p值和一個取值範圍在(0,1)的一個隨機數r,進行比大小,只有p值大於r時,counter的值才會被加一

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

lfu-decay-time是一個以分鐘為單位的數值,可以調整counter的減少速度
#redis部分原始碼展示

double r = (double)rand()/RAND_MAX;

double p = 1.0/(baseval*server.lfu_log_factor+1);

if (r < p) counter++;

其中 baseval是計數器的當前值。計數器的預設初始值為5(由程式碼中的 LFU_INIT_VAL 常量設定),並不是為0,這樣可以避免資料剛進入快取,就因為訪問次數少而被立即淘汰。

當lfu_log_factor取不同的值時,實際訪問次數下,counter的值的變化情況:

在實際的使用場景中,還會有這樣一種情況,某些資料可能一開始會被大量的訪問,但是過了時間段後,就不會再被訪問。

如果這個時候counter的值很大,就算後續不會被訪問,也就不會被redis進行資料淘汰。

針對這種情況,在redis中,設計了counter的衰減策略。其實現就是根據lfu_decay_time的配置值,來控制訪問次數的衰減。

其流程如下:

  • lfu會計算當前時間和資料最近一次訪問的時間差值,並將這個差值換算為分鐘單位。
  • 然後在將這個差值除以lfu_decay_time值,得到的就是我們需要減去的值
  • 然後再講counter的值減去這個值

這樣就可以保證在一段時間後,可以淘汰這部分資料。

Redis 的淘汰策略怎麼選:

一般來說,有這樣一些常用的經驗:

  • 在所有的 key 都是最近最經常使用,那麼就需要選擇 allkeys-lru 進行置換最近最不經常使用的 key,如果你不確定使用哪種策略,那麼推薦使用 allkeys-lru
  • 如果所有的 key 的訪問概率都是差不多的,那麼可以選用 allkeys-random 策略去置換資料
  • 如果對資料有足夠的瞭解,能夠為 key 指定 hint(通過expire/ttl指定),那麼可以選擇 volatile-ttl 進行置換

volatile-lru 和 volatile-random 經常在一個Redis例項既做cache又做持久化的情況下用到,然而,更好的選擇使用兩個Redis例項來解決這個問題。

設定是失效時間 expire 會佔用一些記憶體,而採用 allkeys-lru 就沒有必要設定失效時間,進而更有效的利用記憶體。