通過EXPIRE key seconds 命令來設置數據的過期時間。返回1表明設置成功,返回0表明key不存在或者不能成功設置過期時間。在key上設置了過期時間后key將在指定的秒數后被自動刪除。被指定了過期時間的key在Redis中被稱為是不穩定的。
Redis key過期的方式有三種:
- 惰性刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key(無法保證冷數據被及時刪掉)
- 定期刪除:Redis會定期主動淘汰一批已過期的key(隨機抽取一批key檢查)
- 內存淘汰機制:當前已用內存超過maxmemory限定時,觸發主動清理策略
1、如果沒有設置有效期,即使內存用完,redis 自動回收機制也是看設置了有效期的,不會動沒有設定有效期的,如果清理后內存還是滿的,就不再接受寫操作。
-
Redis無論有沒有設置expire,他都會遵循redis的配置好的刪除機制,在配置文件里設置:
-
redis最大內存不足
"時,數據清除策略,默認為"volatile-lru
"。
-
volatile-lru ->對"過期集合
"中的數據采取LRU(近期最少使用)算法.如果對key使用"expire
"指令指定了過期時間,那么此key將會被添加到"過期集合
"中。將已經過期/LRU的數據優先移除.如果"過期集合
"中全部移除仍不能滿足內存需求,將OOM.
-
allkeys-lru ->對所有的數據,采用LRU算法
-
volatile-random ->對"過期集合
"中的數據采取"隨即選取
"算法,並移除選中的K-V,直到"內存足夠
"為止. 如果如果"過期集合
"中全部移除全部移除仍不能滿足,將OOM
-
allkeys-random ->對所有的數據,采取"隨機選取
"算法,並移除選中的K-V,直到"內存足夠
"為止
-
volatile-ttl ->對"過期集合
"中的數據采取TTL算法(最小存活時間),移除即將過期的數據.
-
noeviction ->不做任何干擾操作,直接返回OOM異常
-
另外,如果數據的過期不會對"應用系統"帶來異常,且系統中write操作比較密集,建議采取"allkeys-lru"。
由以上可以看出,對沒設置expire的數據,產生影響的是allkeys-lru機制,allkeys-random。
所以redis沒設置expire的數據是否會刪除,是由你自己選擇的刪除機制決定的。
在Redis服務器占用內存達到maxmemory最大的情況下,當再想增加內存占用時,會按maxmemory-policy刪除機制將老的數據刪除。這里簡單說一下volatile-lru,Redis會按LRU算法刪除設置了過期時間但還沒有過期的key,而對於沒有設置過期時間的key,Redis是永遠保留的。當然,如果你不想刪除沒有過期的key,那可以使用noeviction機制。
===================================================================
redis如何刪除過期數據?
用一個可以 "find reference" 的 IDE, 沿着 setex( Set the value and expiration of a key ) 命令一窺究竟:
-
void setexCommand(redisClient *c) {
-
c->argv[
3] = tryObjectEncoding(c->argv[
3]);
-
setGenericCommand(c,
0,c->argv[
1],c->argv[
3],c->argv[
2]);
-
}
setGenericCommand 是一個實現 set,setnx,setex 的通用函數,參數設置不同而已。
-
void setCommand(redisClient *c) {
-
c->argv[
2] = tryObjectEncoding(c->argv[
2]);
-
setGenericCommand(c,
0,c->argv[
1],c->argv[
2],NULL);
-
}
-
-
void setnxCommand(redisClient *c) {
-
c->argv[
2] = tryObjectEncoding(c->argv[
2]);
-
setGenericCommand(c,
1,c->argv[
1],c->argv[
2],NULL);
-
}
-
-
void setexCommand(redisClient *c) {
-
c->argv[
3] = tryObjectEncoding(c->argv[
3]);
-
setGenericCommand(c,
0,c->argv[
1],c->argv[
3],c->argv[
2]);
-
}
再看 setGenericCommand :
-
void setGenericCommand(redisClient *c, int nx, robj *key, robj *val, robj *expire) {
-
long seconds =
0;
/* initialized to avoid an harmness warning */
-
-
if (expire) {
-
if (getLongFromObjectOrReply(c, expire, &seconds, NULL) != REDIS_OK)
-
return;
-
if (seconds <=
0) {
-
addReplyError(c,
"invalid expire time in SETEX");
-
return;
-
}
-
}
-
-
if (lookupKeyWrite(c->db,key) != NULL && nx) {
-
addReply(c,shared.czero);
-
return;
-
}
-
setKey(c->db,key,val);
-
server.dirty++;
-
if (expire) setExpire(c->db,key,time(NULL)+seconds);
-
addReply(c, nx ? shared.cone : shared.ok);
-
}
13 行處理 "Set the value of a key, only if the key does not exist" 的場景, 17 行插入這個key , 19 行設置它的超時,注意時間戳已經被設置成了到期時間。這里要看一下 redisDb ( 即 c ->db) 的定義:
-
typedef struct redisDb {
-
dict *dict;
/* The keyspace for this DB */
-
dict *expires;
/* Timeout of keys with a timeout set */
-
dict *blocking_keys;
/* Keys with clients waiting for data (BLPOP) */
-
dict *io_keys;
/* Keys with clients waiting for VM I/O */
-
dict *watched_keys;
/* WATCHED keys for MULTI/EXEC CAS */
-
int id;
-
} redisDb;
僅關注 dict 和 expires ,分別來存 key-value 和它的超時,也就是說如果一個 key-value 是有超時的,那么它會存在 dict 里,同時也存到 expires 里,類似這樣的形式:dict[key]:value,expires[key]:timeout.
當然 key-value 沒有超時, expires 里就不存在這個 key 。 剩下 setKey 和 setExpire 兩個函數無非是插數據到兩個字典里,這里不再詳述。
那么 redis 是如何刪除過期 key 的呢。
通過查看 dbDelete 的調用者, 首先注意到這一個函數,是用來刪除過期 key 的。
-
int expireIfNeeded(redisDb *db, robj *key) {
-
time_t when = getExpire(db,key);
-
-
if (when <
0)
return
0;
/* No expire for this key */
-
-
/* Don't expire anything while loading. It will be done later. */
-
if (server.loading)
return
0;
-
-
/* If we are running in the context of a slave, return ASAP:
-
* the slave key expiration is controlled by the master that will
-
* send us synthesized DEL operations for expired keys.
-
*
-
* Still we try to return the right information to the caller,
-
* that is, 0 if we think the key should be still valid, 1 if
-
* we think the key is expired at this time. */
-
if (server.masterhost != NULL) {
-
return time(NULL) > when;
-
}
-
-
/* Return when this key has not expired */
-
if (time(NULL) <= when)
return
0;
-
-
/* Delete the key */
-
server.stat_expiredkeys++;
-
propagateExpire(db,key);
-
return dbDelete(db,key);
-
}
ifNeed 表示能刪則刪,所以 4 行沒有設置超時不刪, 7 行在 "loading" 時不刪, 16 行非主庫不刪,21 行未到期不刪。 25 行同步從庫和文件。
再看看哪些函數調用了 expireIfNeeded ,有 lookupKeyRead , lookupKeyWrite ,dbRandomKey , existsCommand , keysCommand 。通過這些函數命名可以看出,只要訪問了某一個 key ,順帶做的事情就是嘗試查看過期並刪除,這就保證了用戶不可能訪問到過期的 key 。但是如果有大量的 key 過期,並且沒有被訪問到,那么就浪費了許多內存。 Redis 是如何處理這個問題的呢。
dbDelete 的調用者里還發現這樣一個函數:
-
1
/* Try to expire a few timed out keys. The algorithm used is adaptive and
-
2 * will use few CPU cycles if there are few expiring keys, otherwise
-
3 * it will get more aggressive to avoid that too much memory is used by
-
4 * keys that can be removed from the keyspace. */
-
5
void activeExpireCycle(void) {
-
6
int j;
-
7
-
8
for (j =
0; j < server.dbnum; j++) {
-
9
int expired;
-
10 redisDb *db = server.db+j;
-
11
-
12
/* Continue to expire if at the end of the cycle more than 25%
-
13 * of the keys were expired. */
-
14
do {
-
15
long num = dictSize(db->expires);
-
16 time_t now = time(NULL);
-
17
-
18 expired =
0;
-
19
if (num > REDIS_EXPIRELOOKUPS_PER_CRON)
-
20 num = REDIS_EXPIRELOOKUPS_PER_CRON;
-
21
while (num--) {
-
22 dictEntry *de;
-
23 time_t t;
-
24
-
25
if ((de = dictGetRandomKey(db->expires)) == NULL)
break;
-
26 t = (time_t) dictGetEntryVal(de);
-
27
if (now > t) {
-
28 sds key = dictGetEntryKey(de);
-
29 robj *keyobj = createStringObject(key,sdslen(key));
-
30
-
31 propagateExpire(db,keyobj);
-
32 dbDelete(db,keyobj);
-
33 decrRefCount(keyobj);
-
34 expired++;
-
35 server.stat_expiredkeys++;
-
36 }
-
37 }
-
38 }
while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/
4);
-
39 }
-
40 }
-
41
這個函數的意圖已經有說明: 刪一點點過期 key ,如果過期 key 較少,那也只用一點點 cpu 。 25 行隨機取一個 key , 38 行刪 key 成功的概率較低就退出。這個函數被放在一個 cron 里,每毫秒被調用一次。這個算法保證每次會刪除一定比例的 key ,但是如果 key 總量很大,而這個比例控制的太大,就需要更多次的循環,浪費 cpu ,控制的太小,過期的 key 就會變多,浪費內存——這就是時空權衡了。
最后在 dbDelete 的調用者里還發現這樣一個函數:
-
/* This function gets called when 'maxmemory' is set on the config file to limit
-
* the max memory used by the server, and we are out of memory.
-
* This function will try to, in order:
-
*
-
* - Free objects from the free list
-
* - Try to remove keys with an EXPIRE set
-
*
-
* It is not possible to free enough memory to reach used-memory < maxmemory
-
* the server will start refusing commands that will enlarge even more the
-
* memory usage.
-
*/
-
void freeMemoryIfNeeded(
void)
這個函數太長就不再詳述了,注釋部分說明只有在配置文件中設置了最大內存時候才會調用這個函數,而設置這個參數的意義是,你把 redis 當做一個內存 cache 而不是 key-value 數據庫。
以上 3 種刪除過期 key 的途徑,第二種定期刪除一定比例的 key 是主要的刪除途徑,第一種“讀時刪除”保證過期 key 不會被訪問到,第三種是一個當內存超出設定時的暴力手段。由此也能看出 redis 設計的巧妙之處。