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

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

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

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

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

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

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

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

- 淺灰色是被淘汰的數(shù)據(jù)
- 灰色是沒有被淘汰掉的老數(shù)據(jù)
- 綠色是新加入的數(shù)據(jù)
可以看到3.0的效果明顯比2.8的要得多,并且取樣數(shù)越大,越接近標(biāo)準(zhǔn)的LRU算法
為什么Redis不使用真正的LRU ?
原因很簡單,理論的LRU需要你占用更大的內(nèi)存(每個key還需要保存前后key的地址), 但你從上圖就可以看出Redis 3.0使用的近似LRU算法使用起來的效果幾乎與理論的LRU等效了。
java實現(xiàn)LRU ?
Java自帶的集合框架非常強大,實現(xiàn)LRU算法可以直接使用LinkedHashMap集合框架,簡單實現(xiàn)的話,只需要重寫 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的一個構(gòu)造函數(shù),傳入的第三個參數(shù)accessOrder為true的時候,就按訪問順序?qū)inkedHashMap排序,為false的時候就按插入順序,默認(rèn)是為false的。當(dāng)把accessOrder設(shè)置為true后,就可以將最近訪問的元素置于最前面。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}這是LinkedHashMap中另外一個方法,當(dāng)返回true的時候,就會remove其中最久的元素,可以通過重寫這個方法來控制緩存元素的刪除,當(dāng)緩存滿了后,就可以通過返回true刪除最久未被使用的元素,達(dá)到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
到此這篇關(guān)于淺談Redis內(nèi)存回收策略的文章就介紹到這了,更多相關(guān)Redis內(nèi)存回收內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis+Lua腳本實現(xiàn)計數(shù)器接口防刷功能(升級版)
這篇文章主要介紹了Redis+Lua腳本實現(xiàn)計數(shù)器接口防刷功能,使用腳本使得set命令和expire命令一同達(dá)到Redis被執(zhí)行且不會被干擾,在很大程度上保證了原子操作,對Redis實現(xiàn)計數(shù)器接口防刷功能感興趣的朋友一起看看吧2022-02-02
Redis實戰(zhàn)之Redis實現(xiàn)異步秒殺優(yōu)化詳解
這篇文章主要給大家介紹了Redis實戰(zhàn)之Redis實現(xiàn)異步秒殺優(yōu)化方法,文章通過圖片和代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,感興趣的同學(xué)可以自己動手試一下2023-09-09
redis事務(wù)_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了redis事務(wù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08
Redis高效查詢大數(shù)據(jù)的實踐與優(yōu)化詳細(xì)指南
Redis 是一種高性能的鍵值存儲數(shù)據(jù)庫,廣泛應(yīng)用于緩存,排行榜,計數(shù)器等場景,本文將圍繞如何高效查詢Redis中滿足條件的數(shù)據(jù)展開討論,感興趣的小伙伴可以了解下2025-04-04
windows環(huán)境下Redis+Spring緩存實例講解
這篇文章主要為大家詳細(xì)介紹了windows環(huán)境下Redis+Spring緩存實例教程,感興趣的小伙伴們可以參考一下2016-04-04

