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