做爲一臺服務器來講,內存並非無限的,因此總會存在內存耗盡的狀況,那麼當 Redis
服務器的內存耗盡後,若是繼續執行請求命令,Redis
會如何處理呢?面試
使用Redis
服務時,不少狀況下某些鍵值對只會在特定的時間內有效,爲了防止這種類型的數據一直佔有內存,咱們能夠給鍵值對設置有效期。Redis
中能夠經過 4
個獨立的命令來給一個鍵設置過時時間:10多套Java面試文檔,地址:2021年面試必問的Java面試題redis
expire key ttl
:將 key
值的過時時間設置爲 ttl
秒。pexpire key ttl
:將 key
值的過時時間設置爲 ttl
毫秒。expireat key timestamp
:將 key
值的過時時間設置爲指定的 timestamp
秒數。pexpireat key timestamp
:將 key
值的過時時間設置爲指定的 timestamp
毫秒數。PS:無論使用哪個命令,最終 Redis
底層都是使用 pexpireat
命令來實現的。另外,set
等命令也能夠設置 key
的同時加上過時時間,這樣能夠保證設值和設過時時間的原子性。算法
設置了有效期後,能夠經過 ttl
和 pttl
兩個命令來查詢剩餘過時時間(若是未設置過時時間則下面兩個命令返回 -1
,若是設置了一個非法的過時時間,則都返回 -2
):服務器
ttl key
返回 key
剩餘過時秒數。pttl key
返回 key
剩餘過時的毫秒數。若是將一個過時的鍵刪除,咱們通常都會有三種策略:markdown
爲每一個鍵設置一個定時器,一旦過時時間到了,則將鍵刪除。這種策略對內存很友好,可是對 CPU
不友好,由於每一個定時器都會佔用必定的 CPU
資源。函數
無論鍵有沒有過時都不主動刪除,等到每次去獲取鍵時再判斷是否過時,若是過時就刪除該鍵,不然返回鍵對應的值。這種策略對內存不夠友好,可能會浪費不少內存。oop
系統每隔一段時間就按期掃描一次,發現過時的鍵就進行刪除。這種策略相對來講是上面兩種策略的折中方案,須要注意的是這個按期的頻率要結合實際狀況掌控好,使用這種方案有一個缺陷就是可能會出現已通過期的鍵也被返回。性能
在 Redis
當中,其選擇的是策略 2
和策略 3
的綜合使用。不過 Redis
的按期掃描只會掃描設置了過時時間的鍵,由於設置了過時時間的鍵 Redis
會單獨存儲,因此不會出現掃描全部鍵的狀況:優化
typedef struct redisDb {
dict *dict; //全部的鍵值對
dict *expires; //設置了過時時間的鍵值對
dict *blocking_keys; //被阻塞的key,如客戶端執行BLPOP等阻塞指令時
dict *watched_keys; //WATCHED keys
int id; //Database ID
//... 省略了其餘屬性
} redisDb;
複製代碼
假如 Redis
當中全部的鍵都沒有過時,並且此時內存滿了,那麼客戶端繼續執行 set
等命令時 Redis
會怎麼處理呢?Redis
當中提供了不一樣的淘汰策略來處理這種場景。編碼
首先 Redis
提供了一個參數 maxmemory
來配置 Redis
最大使用內存:
maxmemory <bytes>
複製代碼
或者也能夠經過命令 config set maxmemory 1GB
來動態修改。 若是沒有設置該參數,那麼在 32
位的操做系統中 Redis
最多使用 3GB
內存,而在 64
位的操做系統 中則不做限制。
Redis
中提供了 8
種淘汰策略,能夠經過參數 maxmemory-policy
進行配置:
PS:淘汰策略也能夠直接使用命令 config set maxmemory-policy <策略>
來進行動態配置。
LRU
全稱爲:Least Recently Used
。即:最近最長時間未被使用。這個主要針對的是使用時間。
在 Redis
當中,並無採用傳統的 LRU
算法,由於傳統的 LRU
算法存在 2
個問題:
key
值使用很頻繁,可是最近沒被使用,從而被 LRU
算法刪除。爲了不以上 2
個問題,Redis
當中對傳統的 LRU
算法進行了改造,經過抽樣的方式進行刪除。
配置文件中提供了一個屬性 maxmemory_samples 5
,默認值就是 5
,表示隨機抽取 5
個 key
值,而後對這 5
個 key
值按照 LRU
算法進行刪除,因此很明顯,key
值越大,刪除的準確度越高。
對抽樣 LRU
算法和傳統的 LRU
算法,Redis
官網當中有一個對比圖:
左上角第一幅圖表明的是傳統 LRU
算法,能夠看到,當抽樣數達到 10
個(右上角),已經和傳統的 LRU
算法很是接近了。
前面咱們講述字符串對象時,提到了 redisObject
對象中存在一個 lru
屬性:
typedef struct redisObject {
unsigned type:4;//對象類型(4位=0.5字節)
unsigned encoding:4;//編碼(4位=0.5字節)
unsigned lru:LRU_BITS;//記錄對象最後一次被應用程序訪問的時間(24位=3字節)
int refcount;//引用計數。等於0時表示能夠被垃圾回收(32位=4字節)
void *ptr;//指向底層實際的數據存儲結構,如:SDS等(8字節)
} robj;
複製代碼
lru
屬性是建立對象的時候寫入,對象被訪問到時也會進行更新。正常人的思路就是最後決定要不要刪除某一個鍵確定是用當前時間戳減去 lru
,差值最大的就優先被刪除。可是 Redis
裏面並非這麼作的,Redis
中維護了一個全局屬性 lru_clock
,這個屬性是經過一個全局函數 serverCron
每隔 100
毫秒執行一次來更新的,記錄的是當前 unix
時間戳。
最後決定刪除的數據是經過 lru_clock
減去對象的 lru
屬性而得出的。那麼爲何 Redis
要這麼作呢?直接取全局時間不是更準確嗎?
這是由於這麼作能夠避免每次更新對象的 lru
屬性的時候能夠直接取全局屬性,而不須要去調用系統函數來獲取系統時間,從而提高效率(Redis
當中有不少這種細節考慮來提高性能,能夠說是對性能儘量的優化到極致)。
不過這裏還有一個問題,咱們看到,redisObject
對象中的 lru
屬性只有 24
位,24
位只能存儲 194
天的時間戳大小,一旦超過 194
天以後就會從新從 0
開始計算,因此這時候就可能會出現 redisObject
對象中的 lru
屬性大於全局的 lru_clock
屬性的狀況。
正由於如此,因此計算的時候也須要分爲 2
種狀況:
lruclock
> lru
,則使用 lruclock
- lru
獲得空閒時間。lruclock
< lru
,則使用 lruclock_max
(即 194
天) - lru
+ lruclock
獲得空閒時間。須要注意的是,這種計算方式並不能保證抽樣的數據中必定能刪除空閒時間最長的。這是由於首先超過 194
天還不被使用的狀況不多,再次只有 lruclock
第 2
輪繼續超過 lru
屬性時,計算纔會出問題。
好比對象 A
記錄的 lru
是 1
天,而 lruclock
第二輪都到 10
天了,這時候就會致使計算結果只有 10-1=9
天,實際上應該是 194+10-1=203
天。可是這種狀況能夠說又是更少發生,因此說這種處理方式是可能存在刪除不許確的狀況,可是自己這種算法就是一種近似的算法,因此並不會有太大影響。
LFU
全稱爲:Least Frequently Used
。即:最近最少頻率使用,這個主要針對的是使用頻率。這個屬性也是記錄在redisObject
中的 lru
屬性內。
當咱們採用 LFU
回收策略時,lru
屬性的高 16
位用來記錄訪問時間(last decrement time:ldt,單位爲分鐘),低 8
位用來記錄訪問頻率(logistic counter:logc),簡稱 counter
。
LFU
計數器每一個鍵只有 8
位,它能表示的最大值是 255
,因此 Redis
使用的是一種基於機率的對數器來實現 counter
的遞增。r
給定一箇舊的訪問頻次,當一個鍵被訪問時,counter
按如下方式遞增:
0
和 1
之間的隨機數 R
。counter
- 初始值(默認爲 5
),獲得一個基礎差值,若是這個差值小於 0
,則直接取 0
,爲了方便計算,把這個差值記爲 baseval
。P
計算公式爲:1/(baseval * lfu_log_factor + 1)
。R < P
時,頻次進行遞增(counter++
)。公式中的 lfu_log_factor
稱之爲對數因子,默認是 10
,能夠經過參數來進行控制:
lfu_log_factor 10
複製代碼
下圖就是對數因子 lfu_log_factor
和頻次 counter
增加的關係圖:
能夠看到,當對數因子 lfu_log_factor
爲 100
時,大概是 10M(1000萬)
次訪問纔會將訪問 counter
增加到 255
,而默認的 10
也能支持到 1M(100萬)
次訪問 counter
才能達到 255
上限,這在大部分場景都是足夠知足需求的。
若是訪問頻次 counter
只是一直在遞增,那麼早晚會所有都到 255
,也就是說 counter
一直遞增不能徹底反應一個 key
的熱度的,因此當某一個 key
一段時間不被訪問以後,counter
也須要對應減小。
counter
的減小速度由參數 lfu-decay-time
進行控制,默認是 1
,單位是分鐘。默認值 1
表示:N
分鐘內沒有訪問,counter
就要減 N
。
lfu-decay-time 1
複製代碼
具體算法以下:
16
位(爲了方便後續計算,這個值記爲 now
)。lru
屬性中的高 16
位(爲了方便後續計算,這個值記爲 ldt
)。lru
> now
時,默認爲過了一個週期(16
位,最大 65535
),則取差值 65535-ldt+now
:當 lru
<= now
時,取差值 now-ldt
(爲了方便後續計算,這個差值記爲 idle_time
)。lfu_decay_time
值,而後計算:idle_time / lfu_decay_time
(爲了方便後續計算,這個值記爲num_periods
)。counter
減小:counter - num_periods
。看起來這麼複雜,其實計算公式就是一句話:取出當前的時間戳和對象中的 lru
屬性進行對比,計算出當前多久沒有被訪問到,好比計算獲得的結果是 100
分鐘沒有被訪問,而後再去除配置參數 lfu_decay_time
,若是這個配置默認爲 1
也便是 100/1=100
,表明 100
分鐘沒訪問,因此 counter
就減小 100
。
本文主要介紹了 Redis
過時鍵的處理策略,以及當服務器內存不夠時 Redis
的 8
種淘汰策略,最後介紹了 Redis
中的兩種主要的淘汰算法 LRU
和 LFU
。