前言:
我們的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種。
- volatile-lru:從已設定過期時間的資料集中挑選最近最少使用的資料淘汰。
- volatile-ttl:從已設定過期時間的資料集中挑選將要過期的資料淘汰。
- volatile-random:從已設定過期時間的資料集中任意選擇資料淘汰。
- volatile-lfu:從已設定過期時間的資料集挑選使用頻率最低的資料淘汰。
- allkeys-lru:從資料集中挑選最近最少使用的資料淘汰
- allkeys-lfu:從資料集中挑選使用頻率最低的資料淘汰。
- allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰
- no-enviction(驅逐):禁止驅逐資料,這也是預設策略。
意思是當記憶體不足以容納新入資料時,新寫入操作就會報錯,請求可以繼續進行,線上任務也不能持續進行,採用no-enviction策略可以保證資料不被丟失。
這八種大體上可以分為4中:
- lru(Least Recently Used,最近最少使用)
- lfu(Least Frequently Used,最不經常使用)、
- random(隨機)
- ttl
設定 maxmemory-policy
的方法 和 設定 maxmemory
方法類似,通過 redis.conf
或是通過 CONFIG SET
動態修改。
選擇合適的置換策略是很重要的,這主要取決於你的應用的訪問模式,當然你也可以動態的修改置換策略;
並通過用 Redis 命令——INFO
去輸出 cache 的命中率情況,進而可以對置換策略進行調優。
置換策略是如何工作的?
- 客戶端執行一條新命令,導致資料庫需要增加資料(比如set key value)
- Redis會檢查記憶體使用,如果記憶體使用超過 maxmemory,就會按照置換策略刪除一些 key
- 新的命令執行成功
我們持續的寫資料會導致記憶體達到或超出上限 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
就沒有必要設定失效時間,進而更有效的利用記憶體。