淺談Redis內存回收策略
Redis的內存回收機制主要體現在以下兩個方面:
- 刪除到達過期時間的鍵對象。
- 內存使用達到
maxmemory上限時觸發(fā)內存溢出控制策略。
過期刪除策略
刪除策略的目標:在內存占用與CPU占用之間尋找一種平衡,顧此失彼都會造成整體redis性能的下降,甚至引發(fā)服務器宕機或
內存泄露。
設置Redis鍵過期時間
先回顧一下Redis 提供的設置過期時間的命令:
- EXPIRE :表示將鍵 key 的生存時間設置為 ttl 秒。
- PEXPIRE :表示將鍵 key 的生存時間設置為 ttl 毫秒。
- EXPIREAT :表示將鍵 key 的生存時間設置為 timestamp 所指定的秒數時間戳。
- PEXPIREAT :表示將鍵 key 的生存時間設置為 timestamp 所指定的毫秒數時間戳。
在Redis內部實現中,前面三個設置過期時間的命令最后都會轉換成最后一個PEXPIREAT 命令來完成。
其他相關命令還有:
- 移除鍵的過期時間 PERSIST :表示將key的過期時間移除。
- 返回鍵的剩余生存時間
- TTL :以秒的單位返回鍵 key 的剩余生存時間。
- PTTL :以毫秒的單位返回鍵 key 的剩余生存時間。
在Redis內部,每當我們設置一個鍵的過期時間時,Redis就會將該鍵帶上過期時間存放到一個過期字典中。當我們查詢一個鍵時,Redis便首先檢查該鍵是否存在過期字典中,如果存在,那就獲取其過期時間。然后將過期時間和當前系統時間進行比對,比系統時間大,那就沒有過期;反之判定該鍵過期。
此外:
對于字符串類型鍵,執(zhí)行set命令會去掉過期時間,這個問題很容易在開發(fā)中被忽視
如下是Redis源碼中,set命令的函數setKey,可以看到最后執(zhí)行了removeExpire(db,key)函數去掉了過期時間:
void setKey(redisDb *db, robj *key, robj *val) {
if (lookupKeyWrite(db,key) == NULL) {
dbAdd(db,key,val);
} else {
dbOverwrite(db,key,val);
}
incrRefCount(val);
// 去掉過期時間
removeExpire(db,key);
signalModifiedKey(db,key);
}- Redis不支持二級數據結構(例如哈希、列表)內部元素的過期功能,例如不能對列表類型的一個元素做過期時間設置。
- setex命令作為set+expire的組合,不但是原子執(zhí)行,同時減少了一次網絡通訊的時間。
過期刪除策略
通常刪除某個key,我們有如下三種方式進行處理
1 定時刪除
在設置某個key 的過期時間同時,我們創(chuàng)建一個定時器,讓定時器在該過期時間到來時,立即執(zhí)行對其進行刪除的操作。

- 優(yōu)點:定時刪除對內存是最友好的,能夠保存內存的key一旦過期就能立即從內存中刪除。
- 缺點:對CPU最不友好,在過期鍵比較多的時候,刪除過期鍵會占用一部分 CPU 時間,對服務器的響應時間和吞吐量造成影響。
2 惰性刪除(Lazy delete)
設置該key 過期時間后,我們不去管它,當需要該key時,我們在檢查其是否過期,如果過期,我們就刪掉它,反之返回該key。
- 優(yōu)點:對 CPU友好,我們只會在使用該鍵時才會進行過期檢查,對于很多用不到的key不用浪費時間進行過期檢查。
- 缺點:對內存不友好,如果一個鍵已經過期,但是一直沒有使用,那么該鍵就會一直存在內存中,如果數據庫中有很多這種使用不到的過期鍵,這些鍵便永遠不會被刪除,內存永遠不會釋放。從而造成內存泄漏。
3 定期刪除
每隔一段時間,我們就對一些key進行檢查,刪除里面過期的key。
優(yōu)點:可以通過限制刪除操作執(zhí)行的時長和頻率來減少刪除操作對 CPU 的影響。另外定期刪除,也能有效釋放過期鍵占用的內存。
缺點:難以確定刪除操作執(zhí)行的時長和頻率。
- 如果執(zhí)行的太頻繁,定期刪除策略變得和定時刪除策略一樣,對CPU不友好。
- 如果執(zhí)行的太少,那又和惰性刪除一樣了,過期鍵占用的內存不會及時得到釋放。
- 另外最重要的是,在獲取某個鍵時,如果某個鍵的過期時間已經到了,但是還沒執(zhí)行定期刪除,那么就會返回這個鍵的值,這是業(yè)務不能忍受的錯誤
Redis 使用的過期刪除策略
Redis所有的鍵都可以設置過期屬性,內部保存在過期字典中。由于進程內保存大量的鍵,維護每個鍵精準的過期刪除機制會導致消耗大量的CPU,對于單線程的Redis來說成本過高,因此Redis采用惰性刪除和定時任務刪除機制實現過期鍵的內存回收。

惰性刪除:Redis的惰性刪除策略由 db.c/expireIfNeeded 函數實現,所有鍵讀寫命令執(zhí)行之前都會調用 expireIfNeeded 函數對其進行檢查,如果過期,則刪除該鍵,然后執(zhí)行鍵不存在的操作;未過期則不作操作,繼續(xù)執(zhí)行原有的命令。
定期刪除:由redis.c/activeExpireCycle 函數實現,函數以一定的頻率運行,每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,并刪除其中的過期鍵。注意:并不是一次運行就檢查所有的庫,所有的鍵,而是隨機檢查一定數量的鍵。
定期刪除函數的運行頻率,在Redis2.6版本中,規(guī)定每秒運行10次,大概100ms運行一次。在Redis2.8版本后,可以通過修改配置文件redis.conf 的 hz 選項來調整這個次數。

看上面對這個參數的解釋,建議不要將這個值設置超過 100,否則會對CPU造成比較大的壓力。
定時任務中刪除過期鍵邏輯采用了自適應算法,根據鍵的過期比例、使用快慢兩種速率模式回收鍵,流程如下圖所示:

內存淘汰策略 (逐出算法)
當Redis所用內存達到maxmemory上限時會觸發(fā)相應的溢出控制策略。
具體策略受maxmemory-policy參數控制,Redis支持8種策略(有關LFU算法的,是從Redis4.0以后版本才有):

- noeviction:默認策略,不會刪除任何數據,拒絕所有寫入操作并返回客戶端錯誤信息(error)OOM command not allowed when used memory,此時Redis只響應讀操作。生產一般不會選用
- allkeys-lru 利用LRU算法移除任何key (不管數據有沒有設置超時屬性,直到騰出足夠空間為止)。
- allkeys-lfu 利用LRU算法移除任何key (不管數據有沒有設置超時屬性,直到騰出足夠空間為止)
- volatile-lru:根據LRU算法刪除設置了超時屬性(expire)的鍵,直到騰出足夠空間為止。如果沒有可刪除的鍵對象,回退到noeviction策略。
- volatile-lfu:根據LFU算法刪除設置了超時屬性(expire)的鍵,直到騰出足夠空間為止。如果沒有可刪除的鍵對象,回退到noeviction策略。
- allkeys-random 無差別的隨機移除,直到騰出足夠空間為止。
- volatile-random:隨機刪除過期鍵,直到騰出足夠空間為止。
- volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過期數據。如果沒有,回退到noeviction策略。
在redis.conf 配置文件中,可以設置淘汰方式:

內存溢出控制策略可以采用config set maxmemory-policy{policy}動態(tài)配置。Redis支持豐富的內存溢出應對策略,可以根據實際需求靈活定制,比如當設置volatile-lru策略時,保證具有過期屬性的鍵可以根據LRU剔除,而未設置超時的鍵可以永久保留。還可以采用allkeys-lru策略把Redis變?yōu)榧兙彺娣掌魇褂?。當Redis因為內存溢出刪除鍵時,可以通過執(zhí)行info stats命令查看evicted_keys指標找出當前Redis服務器已剔除的鍵數量。
LFU
LFU 算法(Least Frequently Used,最不經常使用):淘汰最近一段時間被訪問次數最少的數據,以次數作為參考。
需要指出的是 : LRU 算法或者 TTL 算法都是不是很精確算法,而是一個近似的算法。 Redis 不會通過對全部的鍵值對進行比較來確定最精確的時間值,從而確定刪除哪個鍵值對 , 因為這將消耗太多的時間 , 導致回收垃圾執(zhí)行的時間太長 , 造成服務停頓。
當存在熱點數據時,LRU的效率很好,但偶發(fā)性的、周期性的批量操作會導致LRU命中率急劇下降,緩存污染情況比較嚴重。這時使用LFU可能更好點
LRU
LRU算法, 最近最久未使用算法, Least Recently Used
下圖是一個淘汰的流程:

在Redis中LRU算法是一個近似算法,默認情況下,Redis隨機挑選5個鍵,并且從中選取一個最近最久未使用的key進行淘汰,在配置文件中可以通過maxmemory-samples的值來設置redis需要檢查key的個數,但是檢查的越多,耗費的時間也就越久,但是結構越精確(也就是Redis從內存中淘汰的對象未使用的時間也就越久),設置多少,綜合權衡吧~~
Redis 3.0對這個近似算法的優(yōu)化
新算法會維護一個候選池(大小為16),池中的數據根據訪問時間進行排序,第一次隨機選取的key都會放入池中,隨后每次隨機選取的key只有在訪問時間小于池中最小的時間才會放入池中,直到候選池被放滿。當放滿后,如果有新的key需要放入,則將池中最后訪問時間最大(最近被訪問)的移除。當需要淘汰時,需要從池中撈出最久沒被訪問的key淘汰掉就行了。
新舊算法的對比
下面的圖片是Redis官方文檔給出的新舊算法對比結果:

- 淺灰色是被淘汰的數據
- 灰色是沒有被淘汰掉的老數據
- 綠色是新加入的數據
可以看到3.0的效果明顯比2.8的要得多,并且取樣數越大,越接近標準的LRU算法
為什么Redis不使用真正的LRU ?
原因很簡單,理論的LRU需要你占用更大的內存(每個key還需要保存前后key的地址), 但你從上圖就可以看出Redis 3.0使用的近似LRU算法使用起來的效果幾乎與理論的LRU等效了。
java實現LRU ?
Java自帶的集合框架非常強大,實現LRU算法可以直接使用LinkedHashMap集合框架,簡單實現的話,只需要重寫 removeEldestEntry 方法即可。
import java.util.LinkedHashMap;
import java.util.Map.Entry;
public class LRUCache extends LinkedHashMap {
private static final long serialVersionUID = 1L;
private final int capacity;
private long accessCount = 0;
private long hitCount = 0;
public LRUCache(int capacity) {
super(capacity+1, 1.1f, true);
this.capacity = capacity;
}
public String get(String key) {
accessCount++;
if (super.containsKey(key)) {
hitCount++;
}
String value = (String)super.get(key);
return value;
}
public boolean containsKey(String key) {
accessCount++;
if (super.containsKey(key)) {
hitCount++;
return true;
} else {
return false;
}
}
protected boolean removeEldestEntry(Entry eldest) {
return size() > capacity;
}
public long getAccessCount() {
return accessCount;
}
public long getHitCount() {
return hitCount;
}
}這是LinkedHashMap的一個構造函數,傳入的第三個參數accessOrder為true的時候,就按訪問順序對LinkedHashMap排序,為false的時候就按插入順序,默認是為false的。當把accessOrder設置為true后,就可以將最近訪問的元素置于最前面。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}這是LinkedHashMap中另外一個方法,當返回true的時候,就會remove其中最久的元素,可以通過重寫這個方法來控制緩存元素的刪除,當緩存滿了后,就可以通過返回true刪除最久未被使用的元素,達到LRU的要求。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}參考
《Redis 開發(fā)與運維》
http://antirez.com/news/109
https://redis.io/docs/manual/eviction/
https://zhuanlan.zhihu.com/p/149528273
https://blog.51cto.com/u_15239532/2835914
https://www.geekxh.com/1.99.其他補充題目/11.htm
https://www.cnblogs.com/ysocean/p/12422635.html
https://blog.csdn.net/weixin_43230682/article/details/107670911
到此這篇關于淺談Redis內存回收策略的文章就介紹到這了,更多相關Redis內存回收內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis實戰(zhàn)之Redis實現異步秒殺優(yōu)化詳解
這篇文章主要給大家介紹了Redis實戰(zhàn)之Redis實現異步秒殺優(yōu)化方法,文章通過圖片和代碼介紹的非常詳細,對大家的學習或工作有一定的幫助,感興趣的同學可以自己動手試一下2023-09-09
windows環(huán)境下Redis+Spring緩存實例講解
這篇文章主要為大家詳細介紹了windows環(huán)境下Redis+Spring緩存實例教程,感興趣的小伙伴們可以參考一下2016-04-04

