如果一個鍵過期了,那么它什么時候會被刪除呢?
這個問題有三種可能的答案,它們分別代表了三種不同的刪除策略:
- 定時刪除:在設(shè)置鍵的過期時間的同時,創(chuàng)建一個定時器( timer ). 讓定時器在鍵的過期時間來臨時,立即執(zhí)行對鍵的刪除操作。
- 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
- 定期刪除: 每隔一段時間,程序就對數(shù)據(jù)庫進行一次檢查,刪除里面的過期鍵。至于要刪除多少過期鍵,以及要檢查多少個數(shù)據(jù)庫, 則由算法決定。
在這三種策略中,第一種和第三種為主動刪除策略, 而第二種則為被動刪除策略。
前言
使用Redis時我們可以使用EXPIRE或EXPIREAT命令給key設(shè)置過期刪除時間,結(jié)構(gòu)體redisDb中的expires字典保存了所有key的過期時間,這個字典(dict)的key是一個指針,指向redis中的某個key對象,過期字典的value是一個保存過期時間的整數(shù)。
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* 過期字典*/
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 */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
設(shè)置過期時間
不論是EXPIRE,EXPIREAT,還是PEXPIRE,PEXPIREAT,底層的具體實現(xiàn)是一樣的。在Redis的key空間中找到要設(shè)置過期時間的這個key,然后將這個entry(key的指針,過期時間)加入到過期字典中。
void setExpire(redisDb *db, robj *key, long long when) {
dictEntry *kde, *de;
/* Reuse the sds from the main dict in the expire dict */
kde = dictFind(db->dict,key->ptr);
redisAssertWithInfo(NULL,key,kde != NULL);
de = dictReplaceRaw(db->expires,dictGetKey(kde));
dictSetSignedIntegerVal(de,when);
}

過期刪除策略
如果一個key過期了,何時會被刪除呢?在Redis中有兩種過期刪除策略:(1)惰性過期刪除;(2)定期刪除。接下來具體看看。
惰性過期刪除
Redis在執(zhí)行任何讀寫命令時都會先找到這個key,惰性刪除就作為一個切入點放在查找key之前,如果key過期了就刪除這個key。

robj *lookupKeyRead(redisDb *db, robj *key) {
robj *val;
expireIfNeeded(db,key); // 切入點
val = lookupKey(db,key);
if (val == NULL)
server.stat_keyspace_misses++;
else
server.stat_keyspace_hits++;
return val;
}
定期刪除
key的定期刪除會在Redis的周期性執(zhí)行任務(wù)(serverCron,默認每100ms執(zhí)行一次)中進行,而且是發(fā)生Redis的master節(jié)點,因為slave節(jié)點會通過主節(jié)點的DEL命令同步過來達到刪除key的目的。

依次遍歷每個db(默認配置數(shù)是16),針對每個db,每次循環(huán)隨機選擇20個(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)key判斷是否過期,如果一輪所選的key少于25%過期,則終止迭次,此外在迭代過程中如果超過了一定的時間限制則終止過期刪除這一過程。
for (j = 0; j dbs_per_call; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
/* Increment the DB now so we are sure if we run out of time
* in the current DB we'll restart from the next. This allows to
* distribute the time evenly across DBs. */
current_db++;
/* Continue to expire if at the end of the cycle more than 25%
* of the keys were expired. */
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
/* 如果該db沒有設(shè)置過期key,則繼續(xù)看下個db*/
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime();
/* When there are less than 1% filled slots getting random
* keys is expensive, so stop here waiting for better times...
* The dictionary will be resized asap. */
if (num slots > DICT_HT_INITIAL_SIZE
(num*100/slots 1)) break;
/* The main collection cycle. Sample random keys among keys
* with an expire set, checking for expired ones. */
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;// 20
while (num--) {
dictEntry *de;
long long ttl;
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl > 0) {
/* We want the average TTL of keys yet not expired. */
ttl_sum += ttl;
ttl_samples++;
}
}
/* Update the average TTL stats for this database. */
if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;
/* Do a simple running average with a few samples.
* We just use the current estimate with a weight of 2%
* and the previous estimate with a weight of 98%. */
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}
/* We can't block forever here even if there are many keys to
* expire. So after a given amount of milliseconds return to the
* caller waiting for the other active expire cycle. */
iteration++;
if ((iteration 0xf) == 0) { /* 每迭代16次檢查一次 */
long long elapsed = ustime()-start;
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
if (elapsed > timelimit) timelimit_exit = 1;
}
// 超過時間限制則退出
if (timelimit_exit) return;
/* 在當前db中,如果少于25%的key過期,則停止繼續(xù)刪除過期key */
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
總結(jié)
惰性刪除:讀寫之前判斷key是否過期
定期刪除:定期抽樣key,判斷是否過期
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
您可能感興趣的文章:- Redis的LRU機制介紹
- Redis中的數(shù)據(jù)過期策略詳解
- 淺談redis的maxmemory設(shè)置以及淘汰策略
- 關(guān)于redis Key淘汰策略的實現(xiàn)方法
- Redis中LRU淘汰策略的深入分析