Redis 內存滿了怎麼辦?

1 簡介

我們知道 Redis 是一個非常常用的內存型數據庫,數據從內存中讀取是它非常高效的原因之一,那麼但是如果有一天,「Redis 分配的內存滿了怎麼辦」?遇到這個面試題不要慌,這種問題我們分爲兩角度回答就可以:

2 增加 Redis 可用內存

這種方法很暴力,也很好用。我們直接通過增加 Redis 可用內存就可以了, 有兩種方式:

1//設置 Redis最大佔用內存大小爲1000M 
2maxmemory 1000mb
3
1//設置redis最大佔用內存大小爲1000M  
2127.0.0.1:6379> config set maxmemory 1000mb
3

這種方法的確立竿見影,Redis 內存總歸受限於機器的內存,也不能無限制的增長。那麼如果沒有辦法再增加 Redis 的可用內存怎麼辦呢?

3 內存淘汰策略

實際上 Redis 定義了「8 種內存淘汰策略」用來處理 Redis 內存滿的情況:

  1. noeviction:直接返回錯誤,不淘汰任何已經存在的 Redis 鍵;

  2. allkeys-lru:所有的鍵使用 LRU 算法進行淘汰;

  3. volatile-lru:有過期時間的使用 LRU 算法進行淘汰;

  4. allkeys-random:隨機刪除 Redis 鍵;

  5. volatile-random:隨機刪除有過期時間的 Redis 鍵;

  6. volatile-ttl:刪除快過期的 Redis 鍵;

  7. volatile-lfu:根據 LFU 算法從有過期時間的鍵刪除;

  8. allkeys-lfu:根據 LFU 算法從所有鍵刪除。

這些內存淘汰策略都很好理解,我們着重講解一下 LRU、LFU、TTL 是如何實現。

3.1 LRU 最佳實踐

LRU 是 Least Recently Used 的縮寫,也就是「最近很少使用」,也可以理解成最久沒有使用。最近剛剛使用過的,後面接着會用到的概率也就越大。由於內存是非常金貴的,導致我們可以存儲在緩存當中的數據是有限的。比如說我們固定只能存儲 1 萬條,當內存滿了之後,緩存每插入一條新數據,都要拋棄一條最長沒有使用的舊數據。我們把上面的內容整理一下,可以得到幾點要求:

所以我們要儘可能的保證查詢效率很高,插入效率很高。我們知道,如果只考慮查詢效率,那麼 hash 表可能就是最優的選擇。如果只考慮插入效率,那麼鏈表必定有它的一席之地。

但是這兩種數據結構單獨使用都有其弊端、那麼說,有沒有一種數據結構,既能夠保證查詢效率,又能夠保證插入效率呢?於是 hash + 鏈表 這種結構出現了。

hash 表用來查詢在鏈表中的數據位置,鏈表負責數據的插入。當新數據插入到鏈表頭部時有兩種情況:

  1. 當鏈表滿的時候,將鏈表尾部的數據丟棄。這個比較簡單,直接將鏈表尾部指針抹去,並且清除對應 hash 中的信息就好了。

  2. 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部。這種情況我們發現,如果命中到鏈表中間節點,我們需要做的是:

這時雙向鏈表的作用也提現出來了。能直接定位到父節點。這效率就很高了。而且由於雙向鏈表有尾指針,所以剔除最後的尾節點也十分方便快捷。

所以最終的解決方案就是採用「哈希表 + 雙向鏈表」的結構。

3.2 LFU 最佳實踐

LFU:Least Frequently Used,最不經常使用策略。在一段時間內,數據被「使用頻次最少」的, 優先被淘汰。最少使用(LFU)是一種用於管理計算機內存的緩存算法,主要是記錄和追蹤內存塊的使用次數。

當緩存已滿並且需要更多空間時,系統將以最低內存塊使用頻率清除內存。採用 LFU 算法的最簡單方法是爲每個加載到緩存的塊分配一個計數器。每次引用該塊時,計數器將增加一。當緩存達到容量並有一個新的內存塊等待插入時,系統將搜索計數器最低的塊並將其從緩存中刪除。

這裏我們提出一種達到 O(1) 時間複雜度的 LFU 實現方案,它支持的操作包括插入、訪問以及刪除。如下圖所示:

由兩個雙向鏈表 + 哈希表組成。上方的雙向鏈表用來計數,下方的雙向鏈表用來記錄存儲的數據。該鏈表的頭節點存儲了數字,哈希表的 value 對象記錄下方雙向鏈表的數據、

我們這裏按照插入的流程走一遍:

這樣當查找,插入時效率都爲 O(1)。

4 Redis TTL 是怎麼實現的

4.1 TTL 存儲的數據結構

4.2 TTL 設置過期時間

TTL 設置 key 過期時間的方法主要是下面 4 個:

1{"expire",expireCommand,3,"w",0,NULL,1,1,1,0,0},
2{"expireat",expireatCommand,3,"w",0,NULL,1,1,1,0,0},
3{"pexpire",pexpireCommand,3,"w",0,NULL,1,1,1,0,0},
4{"pexpireat",pexpireatCommand,3,"w",0,NULL,1,1,1,0,0},
5

4.3 expire expireat pexpire pexpireat

從實際設置過期時間的實現函數來看,相對時間的策略會有一個當前時間作爲基準時間,絕對時間的策略會「以 0 作爲一個基準時間」。

 1void expireCommand(redisClient *c) {
 2    expireGenericCommand(c,mstime(),UNIT_SECONDS);
 3}
 4void expireatCommand(redisClient *c) {
 5    expireGenericCommand(c,0,UNIT_SECONDS);
 6}
 7void pexpireCommand(redisClient *c) {
 8    expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
 9}
10void pexpireatCommand(redisClient *c) {
11    expireGenericCommand(c,0,UNIT_MILLISECONDS);
12}
13

整個過期時間最後都會換算到絕對時間進行存儲,通過公式基準時間 + 過期時間來進行計算。

對於相對時間而言基準時間就是當前時間,對於絕對時間而言相對時間就是 0。中途考慮設置的過期時間是否已經過期,如果已經過期那麼在 master 就會刪除該數據並同步刪除動作到 slave。

正常的設置過期時間是通過 setExpire 方法保存到 dict *expires 對象當中。

 1/* 
 2*
 3* 這個函數是 EXPIRE 、 PEXPIRE 、 EXPIREAT 和 PEXPIREAT 命令的底層實現函數。
 4*
 5* 命令的第二個參數可能是絕對值,也可能是相對值。
 6* 當執行 *AT 命令時, basetime 爲 0 ,在其他情況下,它保存的就是當前的絕對時間。
 7*
 8* unit 用於指定 argv[2] (傳入過期時間)的格式,
 9* 它可以是 UNIT_SECONDS 或 UNIT_MILLISECONDS ,
10* basetime 參數則總是毫秒格式的。
11*/
12void expireGenericCommand(redisClient *c, long long basetime, int unit) {
13   robj *key = c->argv[1], *param = c->argv[2];
14   long long when; /* unix time in milliseconds when the key will expire. */
15   // 取出 when 參數
16   if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
17       return;
18   // 如果傳入的過期時間是以秒爲單位的,那麼將它轉換爲毫秒
19   if (unit == UNIT_SECONDS) when *= 1000;
20   when += basetime;
21   /* No key, return zero. */
22   // 取出鍵
23   if (lookupKeyRead(c->db,key) == NULL) {
24       addReply(c,shared.czero);
25       return;
26   }
27   /* 
28    * 在載入數據時,或者服務器爲附屬節點時,
29    * 即使 EXPIRE 的 TTL 爲負數,或者 EXPIREAT 提供的時間戳已經過期,
30    * 服務器也不會主動刪除這個鍵,而是等待主節點發來顯式的 DEL 命令。
31    *
32    * 程序會繼續將(一個可能已經過期的 TTL)設置爲鍵的過期時間,
33    * 並且等待主節點發來 DEL 命令。
34    */
35   if (when <= mstime() && !server.loading && !server.masterhost) {
36       // when 提供的時間已經過期,服務器爲主節點,並且沒在載入數據
37       robj *aux;
38       redisAssertWithInfo(c,key,dbDelete(c->db,key));
39       server.dirty++;
40       /* Replicate/AOF this as an explicit DEL. */
41       // 傳播 DEL 命令
42       aux = createStringObject("DEL",3);
43       rewriteClientCommandVector(c,2,aux,key);
44       decrRefCount(aux);
45       signalModifiedKey(c->db,key);
46       notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);
47       addReply(c, shared.cone);
48       return;
49   } else {
50       // 設置鍵的過期時間
51       // 如果服務器爲附屬節點,或者服務器正在載入,
52       // 那麼這個 when 有可能已經過期的
53       setExpire(c->db,key,when);
54       addReply(c,shared.cone);
55       signalModifiedKey(c->db,key);
56       notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
57       server.dirty++;
58       return;
59   }
60}
61 setExpire函數主要是對db->expires中的key對應的dictEntry設置過期時間。
62/*
63* 將鍵 key 的過期時間設爲 when
64*/
65void setExpire(redisDb *db, robj *key, long long when) {
66   dictEntry *kde, *de;
67   /* Reuse the sds from the main dict in the expire dict */
68   // 取出鍵
69   kde = dictFind(db->dict,key->ptr);
70   redisAssertWithInfo(NULL,key,kde != NULL);
71   // 根據鍵取出鍵的過期時間
72   de = dictReplaceRaw(db->expires,dictGetKey(kde));
73   // 設置鍵的過期時間
74   // 這裏是直接使用整數值來保存過期時間,不是用 INT 編碼的 String 對象
75   dictSetSignedIntegerVal(de,when);
76}
77

4.4 R****edis 什麼時候執行淘汰策略

在 Redis 中有三種刪除的操作此策略:

5 參考文檔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7UipC5kC18eeSDBWEdYYIg