Redis過期Key刪除策略和內(nèi)存淘汰策略的實(shí)現(xiàn)
Redis之所以性能強(qiáng),最主要的原因就是基于內(nèi)存存儲,然而單節(jié)點(diǎn)的Redis其內(nèi)存大小不宜過大,否則會影響持久化或主從同步的性能。
Redis內(nèi)存滿了,會發(fā)生什么?
- 在Redis的運(yùn)行內(nèi)存達(dá)到了某個(gè)閾值,就會觸發(fā)內(nèi)存淘汰機(jī)制 => 防止把內(nèi)存撐爆,這個(gè)閾值就是我們設(shè)置的最大運(yùn)行內(nèi)存。
我們可以通過修改redis.conf配置文件來設(shè)置Redis的最大內(nèi)存,配置項(xiàng)為maxmemory:
- Redis默認(rèn)情況下是沒有對最大內(nèi)存大小做限制的,默認(rèn)情況下Redis就是根據(jù)你當(dāng)前的服務(wù)器里面,當(dāng)前最多可以申請到的內(nèi)存大小來去做限制!
# 格式: # maxmemory <bytes> # 例如: maxmemory 1gb
當(dāng)內(nèi)存使用達(dá)到上限,就無法存儲更多數(shù)據(jù)了,因此,為了解決這個(gè)問題,Redis內(nèi)部會有兩套內(nèi)存回收的策略:
- 內(nèi)存過期策略
- 內(nèi)存淘汰策略
內(nèi)存過期策略 - 過期key處理 - 過期刪除策略
如何設(shè)置過期時(shí)間?
- expire key n秒
- pexpire key n毫秒
- set key value ex n秒
- set key value nx n毫秒
查看某個(gè)key剩余的存活時(shí)間:TTL key
- 我們可以通過expire / EX命令給Redis的key設(shè)置TTL(過期時(shí)間 / 存活時(shí)間),單位:秒,當(dāng)key的TTL到期以后,即當(dāng)過期時(shí)間到了以后,再次訪問該key時(shí)返回的是nil,說明這個(gè)Key已經(jīng)不存在了,對應(yīng)的內(nèi)存也得到釋放,從而起到內(nèi)存回收的目的。
# 寫入一條數(shù)據(jù) set num 123 # 設(shè)置20秒過期時(shí)間 expire num 20 # 寫入一條數(shù)據(jù)并設(shè)置20s過期時(shí)間 set num EX 20
Redis是如何知道一個(gè)key是否過期呢?
Redis本身是一個(gè)典型的key-value的鍵值型內(nèi)存存儲數(shù)據(jù)庫,因此所有的key-value都保存在Dict結(jié)構(gòu)中,在其redisDb結(jié)構(gòu)體中,有兩個(gè)Dict,也就是哈希表:一個(gè)用來記錄KEY-VALUE鍵值對(當(dāng)然存的不是真正的Key-Value,存儲的其實(shí)是RedisObject對應(yīng)的內(nèi)存地址的指針),另一個(gè)用來記錄key的TTL。
過期字典存儲在 redisDb 結(jié)構(gòu)中,如下:
來看下redisDb的底層源碼:
typedef struct redisDb { dict dict; / The keyspace for this DB , 也就是存放KEY和VALUE的哈希表*/ dict *expires; /* 同樣是哈希表,但保存的是設(shè)置了TTL的KEY,及其到期時(shí)間*/ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ dict *ready_keys; /* Blocked keys that received a PUSH */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS / int id; / Database ID, 0 ~ 15 / long long avg_ttl; / Average TTL, just for stats / unsigned long expires_cursor; / Cursor of the active expire cycle. */ list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */ } redisDb;
每當(dāng)我們對一個(gè)key設(shè)置了過期時(shí)間后,Redis會把該key帶上過期時(shí)間存儲到一個(gè)過期字典(expires dict)中,也就是說「過期字典」保存了數(shù)據(jù)庫中所有 key 的過期時(shí)間。
當(dāng)我們查詢一個(gè) key 時(shí),Redis 首先檢查該 key 是否存在于過期字典中:
- 如果不在,則正常讀取鍵值;
- 如果存在,則會獲取該 key 的過期時(shí)間,然后與當(dāng)前系統(tǒng)時(shí)間進(jìn)行比對,如果比系統(tǒng)時(shí)間大,那就沒有過期,否則判定該 key 已過期 => 查詢到對應(yīng)的TTL,加以判斷即可。
是不是TTL到期就立即刪除了呢?
TTL(Time To Live)的含義是存活時(shí)間。
- Redis并不會在Key過期時(shí)立刻刪除KEY,因?yàn)橐獙?shí)現(xiàn)這樣的效果就必須給每一個(gè)過期Key設(shè)置一個(gè)定時(shí)器,并監(jiān)控這些Key的過期狀態(tài),然后去做判斷,在key過期的那一刻給它立刻刪掉 => 定時(shí)刪除,它的優(yōu)點(diǎn)就是保證過期key會被盡快刪除,也就是內(nèi)存可以盡快得到釋放,因此,定時(shí)刪除對內(nèi)存是最友好的;
- 如果說我們的key比較少那還好,但是如果我們的key非常的多,達(dá)到數(shù)十萬甚至數(shù)百萬,那么這些key如果我們都給它設(shè)置這樣一個(gè)定時(shí)器,無論是對CPU還是對內(nèi)存都會帶來極大的負(fù)擔(dān),這樣一來,就會嚴(yán)重影響到Redis服務(wù)本身的一個(gè)性能,所以說這個(gè)是沒有辦法接受的,因此,我們在實(shí)際應(yīng)用當(dāng)中,Redis采用的并不是立即刪除,而是惰性刪除 + 周期刪除 => Redis 使用的過期刪除策略是「惰性刪除+定期刪除」這兩種策略配和使用,刪除的對象是已過期的 key。
Redis的過期KEY刪除策略有兩種:
惰性刪除
周期刪除或定期刪除
惰性刪除
- 顧名思義就是TTL過期后不會立刻刪除,惰性刪除策略的做法是,不主動刪除過期鍵,而是在訪問使用一個(gè)key的時(shí)候,判斷當(dāng)前key有沒有設(shè)置TTL過期時(shí)間,如果有,則檢查該key的存活時(shí)間,如果發(fā)現(xiàn)key已經(jīng)過期才執(zhí)行刪除操作,如果沒有過期,不做任何處理,然后返回正常的鍵值對給客戶端。
惰性刪除策略的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):
- 因?yàn)槊看卧L問時(shí),才會檢查該key是否過期,因此惰性刪除策略可以節(jié)省CPU資源,對CPU時(shí)間最友好。
缺點(diǎn):
- 如果一個(gè)key已經(jīng)過期,而這個(gè)key又仍然保留在數(shù)據(jù)庫中,那么只要這個(gè)過期key一致沒有被訪問,它所占用的內(nèi)存就不會釋放,會造成一定的內(nèi)存空間浪費(fèi),所以惰性刪除策略對內(nèi)存不友好 => 這不就是會導(dǎo)致內(nèi)存泄漏嗎???
周期刪除 / 定期刪除策略
- 顧名思義就是通過一個(gè)定時(shí)任務(wù),每隔一段時(shí)間周期性的從數(shù)據(jù)庫中抽取一定數(shù)量的key進(jìn)行檢查,并刪除其中的過期key。
Redis默認(rèn)會每秒進(jìn)行10次過期檢查(此配置可以通過Redis的配置文件redis.conf進(jìn)行配置,配置鍵為hz,它的默認(rèn)值是hz 10),每次檢查數(shù)據(jù)庫并不是遍歷過期字典中的所有key,而是從數(shù)據(jù)庫中隨機(jī)抽取一定數(shù)量的key進(jìn)行過期檢查:
- 從過期字典中隨機(jī)抽取20個(gè)key;
- 檢查這20個(gè)key是否過期,并刪除已過期的key;
- 如果本輪檢查的已過期key的數(shù)量,超過5個(gè)(5 / 20 = 1 / 4 = 25%),也就是「已過期 key 的數(shù)量」占比「隨機(jī)抽取 key 的數(shù)量」大于 25%,則繼續(xù)隨機(jī)抽查,重復(fù)步驟1;如果已過期的key的比例小于25%,則停止繼續(xù)刪除過期key,退出本輪檢查,然后等待下一輪再檢查。
可以看到,定期刪除是一個(gè)循環(huán)的流程。
Redis為了保證定期刪除不會出現(xiàn)循環(huán)過度,導(dǎo)致線程卡死現(xiàn)象,為此增加了定期刪除循環(huán)流程的時(shí)間上限,默認(rèn)不會超過25ms,超出時(shí)間限制則退出。
定期刪除策略的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
定期刪除是Redis的主動刪除策略,它可以確保過期key能夠及時(shí)被刪除
缺點(diǎn):
會占用CPU資源去掃描key,可能會影響到Redis的性能
可以看到,惰性刪除策略和定期刪除策略都有各自的優(yōu)缺點(diǎn),所以Redis選擇「惰性刪除+定期刪除」這兩種策略配和使用,以求在合理使用 CPU 時(shí)間和避免內(nèi)存浪費(fèi)之間取得平衡。
如果過期鍵沒有被訪問,而定期刪除又跟不上新鍵產(chǎn)生的速度,內(nèi)存不就慢慢耗盡了嗎?
內(nèi)存淘汰策略
內(nèi)存淘汰:
- 就是當(dāng)Redis的內(nèi)存使用達(dá)到設(shè)置的閾值時(shí),Redis就會主動挑選部分key刪除以釋放更多內(nèi)存的流程,這就叫做內(nèi)存淘汰機(jī)制。
- 內(nèi)存淘汰策略是解決內(nèi)存過大的問題。
內(nèi)存淘汰時(shí)機(jī)
當(dāng)內(nèi)存達(dá)到閾值時(shí)執(zhí)行內(nèi)存淘汰,但問題是Redis什么時(shí)候會去判斷內(nèi)存是否達(dá)到閾值呢?
- Redis每次執(zhí)行任何命令時(shí),都會判斷內(nèi)存是否達(dá)到閾值。
當(dāng)Redis內(nèi)存不足時(shí)會怎么做?
- 這取決于配置的內(nèi)存淘汰策略,Redis支持很多種內(nèi)存淘汰策略,例如LRU、LFU、Random,但默認(rèn)的策略是直接決絕新的寫入請求,而如果設(shè)置了其它策略,則會在每次執(zhí)行命令后判斷內(nèi)存占用是否達(dá)到閾值,如果達(dá)到閾值則會基于配置的內(nèi)存淘汰策略嘗試進(jìn)行內(nèi)存淘汰,直到占用內(nèi)存小于閾值為止。
Redis 內(nèi)存淘汰策略有哪些?
Redis支持內(nèi)存淘汰,配置參數(shù)maxmemory-policy決定了內(nèi)存淘汰策略,這個(gè)參數(shù)一共有8個(gè)枚舉值,也就是說Redis內(nèi)存淘汰策略共有8種,這八種策略大體分為「不進(jìn)行數(shù)據(jù)淘汰」和「進(jìn)行數(shù)據(jù)淘汰」兩類策略:
1. 不進(jìn)行數(shù)據(jù)淘汰的策略
- noeviction(Redis3.0之后,默認(rèn)的內(nèi)存淘汰策略) :禁止刪除數(shù)據(jù),它表示當(dāng)Redis的運(yùn)行內(nèi)存超過最大設(shè)置內(nèi)存時(shí),也就是當(dāng)Redis內(nèi)存滿時(shí),不淘汰任何鍵值對數(shù)據(jù),而是不再提供服務(wù),不允許寫入新數(shù)據(jù),Redis的寫命令會直接返回錯(cuò)誤信息(但是讀命令還是可以正常返回)。
2. 進(jìn)行數(shù)據(jù)淘汰的策略
針對「進(jìn)行數(shù)據(jù)淘汰」這一類策略,又可以細(xì)分為「在設(shè)置了過期時(shí)間的數(shù)據(jù)中進(jìn)行淘汰」和「在所有數(shù)據(jù)范圍內(nèi)進(jìn)行淘汰」這兩類策略。 在設(shè)置了過期時(shí)間的數(shù)據(jù)中進(jìn)行淘汰:
- volatile-random:隨機(jī)淘汰設(shè)置了過期時(shí)間的任意鍵值 - 從已設(shè)置過期時(shí)間的數(shù)據(jù)集中任意選擇數(shù)據(jù)淘汰;
- volatile-ttl:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中挑選將要過期的數(shù)據(jù)淘汰:比較key的剩余TTL值,TTL越小越先被淘汰。
- volatile-lru(Redis3.0 之前,默認(rèn)的內(nèi)存淘汰策略):LRU(Least Recently Used),最近最久未使用,利用LRU算法淘汰所有設(shè)置了TTL過期時(shí)間的鍵值中,最久未使用的鍵值;
- volatile-lfu(Redis 4.0 后新增的內(nèi)存淘汰策略):LFU(Least Frequently Used),最少頻率使用,淘汰所有設(shè)置了TTL過期時(shí)間的鍵值中,最少使用的鍵值;
在所有數(shù)據(jù)范圍內(nèi)進(jìn)行淘汰:
- allkeys-random:對全體key,隨機(jī)進(jìn)行淘汰;
- allkeys-lru:LRU(Least Recently Used),最近最久未使用,淘汰全體鍵值中最久未使用的鍵值;
- allkeys-lfu(Redis 4.0 后新增的內(nèi)存淘汰策略)::LFU(Least Frequently Used),最少頻率使用,淘汰整個(gè)鍵值中最少使用的鍵值,即訪問頻率最低的那個(gè)key-value。
比較容易混淆的有兩個(gè)算法:
- LRU(Least Recently Used),最近最久未使用(根據(jù)訪問時(shí)間淘汰),會選擇淘汰最近最少使用的數(shù)據(jù),用當(dāng)前時(shí)間減去最后一次訪問時(shí)間,這個(gè)值越大則淘汰優(yōu)先級越高。
- LFU(Least Frequently Used),最少頻率使用(根據(jù)訪問頻率淘汰),LFU 算法是根據(jù)數(shù)據(jù)訪問次數(shù)來淘汰數(shù)據(jù)的,它的核心思想是“如果數(shù)據(jù)過去被訪問多次,那么將來被訪問的頻率也更高”,所以, LFU 算法會統(tǒng)計(jì)每個(gè)key的訪問頻率,值越小淘汰優(yōu)先級越高。
Redis 4.0開始支持基于LFU算法的淘汰策略!
Redis為什么新增了LFU淘汰策略?
為什么Redis 4.0有了LFU?
- 比如Redis中的一個(gè)鍵,之前一直都沒有被訪問過,最近突然被訪問了一次,如果使用LRU淘汰策略就很難被淘汰,因?yàn)長RU會把它定義為熱鍵;
- 而使用LFU淘汰策略該key就可能很快被淘汰,因?yàn)長RU優(yōu)先淘汰最近未被使用的,而LFU淘汰的是最近訪問頻率最低的。
- LFU比LRU淘汰更精確,有助于提升Redis的緩存命中率。
Redis怎么知道某個(gè)KEY的最后一次訪問時(shí)間或者是訪問頻率呢?
- redisObject結(jié)構(gòu)體當(dāng)中的lru就是記錄最近一次訪問時(shí)間和訪問頻率的,以低8位無符號數(shù)字來記錄邏輯訪問次數(shù)。
- 邏輯訪問次數(shù)又是怎么回事呢?8位無符號數(shù)字最大才255,訪問次數(shù)超過255怎么辦?
邏輯訪問次數(shù)是如何計(jì)算的?
- 由于記錄訪問次數(shù)的只有8bit,即便是無符號數(shù),最大值只有255,不可能記錄真實(shí)的訪問次數(shù),因此LFU的訪問次數(shù)之所以叫做邏輯訪問次數(shù),是因?yàn)椴⒉皇敲看蝛ey被訪問都計(jì)數(shù),Redis統(tǒng)計(jì)的其實(shí)是邏輯訪問次數(shù),這其中是一個(gè)計(jì)算公式,會根據(jù)當(dāng)前的訪問次數(shù)做計(jì)算,結(jié)果要么是次數(shù) + 1,要么是次數(shù)不變,且最大不超過255,除此以外,邏輯訪問次數(shù)還有一個(gè)衰減周期,訪問次數(shù)會隨時(shí)間衰減,默認(rèn)為1分鐘,即每隔1分鐘邏輯訪問次數(shù)會 -1,這樣邏輯訪問次數(shù)就能基本反映出一個(gè)key的訪問熱度了。
顯然LFU的基于訪問頻率的統(tǒng)計(jì)更符合我們的淘汰目標(biāo),因此官方推薦使用LFU算法。
內(nèi)存淘汰用到的是LRU算法嗎?
- 嗯...Redis使用的是近似LRU算法,傳統(tǒng)LRU算法的實(shí)現(xiàn)需要一個(gè)雙向鏈表來記錄數(shù)據(jù)最近被訪問的順序,最新操作的鍵會被移動到表頭,當(dāng)需要內(nèi)存淘汰時(shí),只需要刪除鏈表尾部的數(shù)據(jù)即可,因?yàn)殒湵砦膊康脑鼐痛碜罹梦幢皇褂玫脑亍?nbsp;
- 但是Redis中的KEY可能有數(shù)百萬甚至更多,出于節(jié)省內(nèi)存的考慮,Redis的LRU算法并非完整的實(shí)現(xiàn),Redis的算法并不是真正的LRU,而是一種基于抽樣的近似LRU算法!
- Redis采用的是抽樣法,即每次抽樣一定數(shù)量(maxmemory-samples)的key,然后和目前維持的淘汰候選池綜合比較,然后基于內(nèi)存策略做排序,找出淘汰優(yōu)先級最高的,刪除這個(gè)key,這就使得算法的結(jié)果更接近于真正的LRU算法了,特別是在抽樣值較高的情況下(例如10),可以達(dá)到與真正的LRU接近的結(jié)果。
當(dāng)Redis作為緩存使用的時(shí)候,推薦使用allkeys-lru淘汰策略,該策略會將最近最久未使用的key淘汰,像這種key后期命中的概率也最低,所以將其淘汰。
到此這篇關(guān)于Redis過期Key刪除策略和內(nèi)存淘汰策略的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis過期Key刪除策略和內(nèi)存淘汰策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Redis實(shí)現(xiàn)分布式單號及分布式ID(自定義規(guī)則生成)
一些業(yè)務(wù)背景下,業(yè)務(wù)要求單號需要有區(qū)分不同的前綴,那么在分布式的架構(gòu)下如何自定義單號而且還能保證唯一呢?本文就來詳細(xì)的介紹一下2021-09-09redis 限制內(nèi)存使用大小的實(shí)現(xiàn)
這篇文章主要介紹了redis 限制內(nèi)存使用大小的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05解決 Redis 數(shù)據(jù)傾斜、熱點(diǎn)等問題
?單臺機(jī)器的硬件配置有上限制約,一般我們會采用分布式架構(gòu)將多臺機(jī)器組成一個(gè)集群,這篇文章主要介紹了解決 Redis 數(shù)據(jù)傾斜、熱點(diǎn)等問題,需要的朋友可以參考下2022-12-12