面試必問:Redis過期Key刪除和內(nèi)存淘汰策略
本文轉(zhuǎn)載自微信公眾號「蟲爸說說」,作者蟲爸 。轉(zhuǎn)載本文請聯(lián)系蟲爸說說公眾號。
眾所周知,Redis是一種內(nèi)存級kv數(shù)據(jù)庫,所有的操作都是在內(nèi)存里面進(jìn)行,定期通過異步操作把數(shù)據(jù)庫數(shù)據(jù)flush到硬盤上進(jìn)行保存。因此它是純內(nèi)存操作,Redis的性能非常出色,每秒可以處理超過10萬次讀寫操作。雖然是內(nèi)存數(shù)據(jù)庫,但是其數(shù)據(jù)可以持久化,而且支持豐富的數(shù)據(jù)類型。
正因?yàn)槭莾?nèi)存級操作,那么其受限于物理內(nèi)存,所以Redis提供了過期key的刪除以及內(nèi)存淘汰策略,從而在一定程度上,能夠避免達(dá)到內(nèi)存上限。
在本文中,我們首先介紹下如何對某個(gè)key設(shè)置過期時(shí)間,然后再次介紹對于這些過期key都有哪些處理策略,隨后分析下在內(nèi)存達(dá)到上限時(shí)候,redis采取的策略。
設(shè)置過期
redis中設(shè)置過期時(shí)間有四種方式:
- expire key seconds:設(shè)置key在N秒后過期;
- pexpire key milliseconds:設(shè)置key在n毫秒后過期;
- expire key timestamp:設(shè)置key在某個(gè)時(shí)間戳后過期(精確到秒)
- pexpireat key millisecondstimestamp:設(shè)置key在一個(gè)時(shí)間戳后過期(精確到毫秒)
下面,我們來看看具體命令的用法。
expire: N秒后過期
- 127.0.0.1:6379> set key value
- OK
- 127.0.0.1:6379> expire key 100
- (integer) 1
- 127.0.0.1:6379> ttl key
- (integer) 93
其中命令TTL的全稱是 time to live,意思是key在N秒后過期。比如上面的結(jié)果93表示key在93s后過期。
pexpire: N毫秒后過期
- 127.0.0.1:6379> set key2 value2
- OK
- 127.0.0.1:6379> pexpire key2 100000
- (integer) 1
- 127.0.0.1:6379> pttl key2
- (integer) 94524
pexpire key2 100000 表示 key2 設(shè)置為在 100000 毫秒(100 秒)后過期。
expireat: 在某個(gè)時(shí)間戳過期(精確到秒)
- 127.0.0.1:6379> set key3 value3
- OK
- 127.0.0.1:6379> expireat key3 1630644399
- (integer) 1
- 127.0.0.1:6379> ttl key3
- (integer) 67
expired Key3 1630644399(精確到秒)之后過期。使用TTL查詢,可以發(fā)現(xiàn)Key3會在67s后過期。
在redis中,可以使用time命令查詢當(dāng)前時(shí)間的時(shí)間戳(精確到秒),例如:
127.0.0.1:6379> time
1) "1630644526"
2) "239640"
pexpireat: 在某個(gè)時(shí)間戳過期(精確到毫秒)
- 127.0.0.1:6379> set key4 value4
- OK
- 127.0.0.1:6379> pexpireat key4 1630644499740
- (integer) 1
- 127.0.0.1:6379> pttl key4
- (integer) 3522
其中,pexpireat key4 1630644499740表示key4在時(shí)間戳1630644499740(精確到毫秒)之后過期。使用TTL查詢可以發(fā)現(xiàn)key4會在3522ms后過期。
value為string時(shí)候的過期設(shè)置
直接操作value為string的過期時(shí)間有幾種方法,如下所示:
- set key value ex seconds:N秒后過期
- set key value ex milliseconds:設(shè)置key在n毫秒后過期;
- setex key seconds value:為指定的 key 設(shè)置值及其過期時(shí)間,如果 key 已經(jīng)存在, SETEX 命令將會替換舊的值。
設(shè)置kv對在N秒后過期
- 127.0.0.1:6379> set k v ex 100
- OK
- 127.0.0.1:6379> ttl k
- (integer) 97
設(shè)置kv對在N毫秒后過期
- 127.0.0.1:6379> set k2 v2 px 100000
- OK
- 127.0.0.1:6379> pttl k2
- (integer) 92483
使用setex來設(shè)置
- 127.0.0.1:6379> setex k3 100 v3
- OK
- 127.0.0.1:6379> ttl k3
- (integer) 91
取消過期
使用命令:persist key去除key值的過期時(shí)間,如下代碼所示:
- 127.0.0.1:6379> ttl k3
- (integer) 97
- 127.0.0.1:6379> persist k3
- (integer) 1
- 127.0.0.1:6379> ttl k3
- (integer) -1
可以看出,第一次使用TTL查詢K3,97s后就會過期。使用persist命令查詢K3的生命周期的結(jié)果是-1,表示K3永不過期。
過期策略
redis對過期key的刪除策略,有定時(shí)刪除、定期刪除和惰性刪除三種。
定時(shí)刪除
創(chuàng)建一個(gè)定時(shí)器,當(dāng)key設(shè)置有過期時(shí)間,且過期時(shí)間到達(dá)時(shí),由定時(shí)器任務(wù)執(zhí)行對key的刪除操作。
- 優(yōu)點(diǎn):節(jié)約內(nèi)存,到時(shí)就刪除,快速釋放掉不必要的內(nèi)存占用
- 缺點(diǎn):CPU壓力很大,無論CPU此時(shí)負(fù)載量多高,均占用CPU,會影響redis服務(wù)器響應(yīng)時(shí)間和指令吞吐量
定期刪除
redis默認(rèn)是每隔100ms就隨機(jī)抽取一些設(shè)置了過期時(shí)間的key,檢查其是否過期,如果過期就刪除。注意這里是隨機(jī)抽取的。為什么要隨機(jī)呢?假如redis存了幾十萬個(gè)key,每隔100ms就遍歷所有的設(shè)置過期時(shí)間的key的話,就會給CPU帶來很大的負(fù)載。
- 優(yōu)點(diǎn):可以通過限制刪除操作執(zhí)行的時(shí)長和頻率來減少刪除操作對 CPU 的影響。另外定期刪除,也能有效釋放過期鍵占用的內(nèi)存。
- 缺點(diǎn):難以確定刪除操作執(zhí)行的時(shí)長和頻率。
如果執(zhí)行的太頻繁,定期刪除策略變得和定時(shí)刪除策略一樣,對CPU不友好,如果執(zhí)行的太少,那又和惰性刪除一樣了,過期鍵占用的內(nèi)存不會及時(shí)得到釋放。
另外最重要的是,在獲取某個(gè)鍵時(shí),如果某個(gè)鍵的過期時(shí)間已經(jīng)到了,但是還沒執(zhí)行定期刪除,那么就會返回這個(gè)鍵的值,這是業(yè)務(wù)不能忍受的錯誤。
惰性刪除
定期刪除可能會導(dǎo)致很多過期key到了時(shí)間并沒有被刪除掉。所以就有了惰性刪除。假如你的過期key,靠定期刪除沒有被刪除掉,還停留在內(nèi)存里,除非你的系統(tǒng)去查一下那個(gè)key,才會被redis給刪除掉。這就是所謂的惰性刪除。expireIfNeeded(),檢查數(shù)據(jù)是否過期,執(zhí)行g(shù)et的時(shí)候調(diào)用。
- 優(yōu)點(diǎn):節(jié)約CPU性能,發(fā)現(xiàn)必須刪除的時(shí)候才刪除。
- 缺點(diǎn):內(nèi)存壓力很大,出現(xiàn)長期占用內(nèi)存的數(shù)據(jù)
換句話說,惰性刪除就是用存儲空間換取處理器性能
結(jié)合上述三種策略的優(yōu)缺點(diǎn),redis采取了折中的刪除策略,即采用的是定期刪除+惰性刪除策略。
1、定時(shí)刪除,用一個(gè)定時(shí)器來負(fù)責(zé)監(jiān)視key,過期則自動刪除。雖然內(nèi)存及時(shí)釋放,但是十分消耗CPU資源。在大并發(fā)請求下,CPU要將時(shí)間應(yīng)用在處理請求,而不是刪除key,因此沒有采用這一策略
定期刪除+惰性刪除是如何工作的呢?
2、定期刪除,redis默認(rèn)每個(gè)100ms檢查,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每個(gè)100ms將所有的key檢查一次,而是隨機(jī)抽取進(jìn)行檢查(如果每隔100ms,全部key進(jìn)行檢查,redis豈不是卡死)。因此,如果只采用定期刪除策略,會導(dǎo)致很多key到時(shí)間沒有刪除。
3、惰性刪除,也就是說在你獲取某個(gè)key的時(shí)候,redis會檢查一下,這個(gè)key如果設(shè)置了過期時(shí)間那么是否過期了?如果過期了此時(shí)就會刪除。
但是這種方案,仍然存在缺點(diǎn): 如果定期刪除沒刪除key。然后你也沒及時(shí)去請求key,也就是說惰性刪除也沒生效。這樣,redis的內(nèi)存會越來越高。那么就應(yīng)該采用內(nèi)存淘汰機(jī)制。
內(nèi)存淘汰策略
maxmemory 用于指定 Redis 能使用的最大內(nèi)存。既可以在 redis.conf 文件中設(shè)置, 也可以在運(yùn)行過程中通過 CONFIG SET 命令動態(tài)修改。
例如, 要設(shè)置 100MB 的內(nèi)存限制, 可以在 redis.conf 文件中這樣配置:
- maxmemory 100mb
上述命令設(shè)置了redis內(nèi)存上限,當(dāng)內(nèi)存中的數(shù)據(jù)量達(dá)到其設(shè)置的上限的時(shí)候,就需要采取一定的淘汰策略,否則會影響redis的正常訪問。
為了更好的實(shí)現(xiàn)這一點(diǎn),必須針對不同的應(yīng)用場景提供不同的策略,下面,我們將介紹下redis支持的幾種內(nèi)存淘汰策略。
Redis 提供了以下幾種策略供用戶選擇,其中noeviction 策略的默認(rèn)策略為。
- noeviction:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),新寫入操作會報(bào)錯。
- allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在鍵空間中,移除最近最少使用的key。
- allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在鍵空間中,隨機(jī)移除某個(gè)key。
- volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間中,移除最近最少使用的key。
- volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間中,隨機(jī)移除某個(gè)key。
- volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間中,有更早過期時(shí)間的key優(yōu)先移除。
需要注意的是,如果沒有設(shè)置 expire 的key, 不滿足先決條件,那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行為, 和 noeviction(不刪除) 基本上一致。
Redis 使用的并不是完全LRU算法。自動驅(qū)逐的 key , 并不一定是最滿足LRU特征的那個(gè). 而是通過近似LRU算法, 抽取少量的 key 樣本, 然后刪除其中訪問時(shí)間最古老的那個(gè)key。
驅(qū)逐算法, 從 Redis 3.0 開始得到了巨大的優(yōu)化, 使用 pool(池子) 來作為候選. 這大大提升了算法效率, 也更接近于真實(shí)的LRU算法。
在 Redis 的 LRU 算法中, 可以通過設(shè)置樣本(sample)的數(shù)量來調(diào)優(yōu)算法精度。
maxmemory-samples 5
以上就是Redis的六種淘汰策略。關(guān)于這六種策略的使用,使用者需要根據(jù)自身實(shí)際需要,選擇合理的淘汰策略。讀者可以根據(jù)自身需求,再結(jié)合下面的筆者經(jīng)驗(yàn),進(jìn)行策略選擇。
- 當(dāng)部分?jǐn)?shù)據(jù)訪問頻率較高而其余部分訪問頻率較低,或者數(shù)據(jù)的使用頻率無法預(yù)測時(shí),設(shè)置allkeys-lru比較合適。
- 如果所有數(shù)據(jù)訪問概率大致相等,可以選擇allkeys-random。
- 如果開發(fā)者需要通過設(shè)置不同的ttls來確定數(shù)據(jù)過期的順序,此時(shí)可以選擇volatile-ttl策略。
- 如果你想讓一些數(shù)據(jù)長期保存,而一些數(shù)據(jù)可以消除,最好選擇volatile-lru或volatile-random。
- 由于設(shè)置expire會消耗額外的內(nèi)存,如果你打算避免Redis內(nèi)存浪費(fèi)在這一項(xiàng)上,可以選擇allkeys-lru策略,這樣就可以不再設(shè)置過期時(shí)間,高效利用內(nèi)存。
經(jīng)驗(yàn)之談
對于redis的操作,我們應(yīng)該慎之又慎。
- 不要放垃圾數(shù)據(jù),及時(shí)清理無用數(shù)據(jù)。
- key盡量都設(shè)置過期時(shí)間。對具有時(shí)效性的key設(shè)置過期時(shí)間,通過redis自身的過期key清理策略來降低過期key對于內(nèi)存的占用,同時(shí)也能夠減少業(yè)務(wù)的麻煩,不需要定期手動清理了。
- 單Key不要過大,這種key在get的時(shí)候網(wǎng)絡(luò)傳輸延遲會比較大,需要分配的輸出緩沖區(qū)也比較大,在定期清理的時(shí)候也容易造成比較高的延遲. 最好能通過業(yè)務(wù)拆分,數(shù)據(jù)壓縮等方式避免這種過大的key的產(chǎn)生。
- 不同業(yè)務(wù)如果公用一個(gè)業(yè)務(wù)的話,最好使用不同的邏輯db分開。這是因?yàn)镽edis的過期Key清理策略和強(qiáng)制淘汰策略都會遍歷各個(gè)db。將key分布在不同的db有助于過期Key的及時(shí)清理。另外不同業(yè)務(wù)使用不同db也有助于問題排查和無用數(shù)據(jù)的及時(shí)下線。