void setex command(redisClient * c){
c->;argv[3]= tryObjectEncoding(c-& gt;argv[3]);
setGenericCommand(c,0,c-& gt;argv[1],c->argv[3],c-& gt;argv[2]);
}
SetGenericCommand是實現set,set,setnx,setex的通用函數,參數設置不同。
void setCommand(redisClient *c) {
c->;argv[2]= tryObjectEncoding(c-& gt;argv[2]);
setGenericCommand(c,0,c-& gt;argv[1],c->argv[2],NULL);
}
void setnx command(redisClient * c){
c->;argv[2]= tryObjectEncoding(c-& gt;argv[2]);
setGenericCommand(c,1,c-& gt;argv[1],c->argv[2],NULL);
}
void setex command(redisClient * c){
c->;argv[3]= tryObjectEncoding(c-& gt;argv[3]);
setGenericCommand(c,0,c-& gt;argv[1],c->argv[3],c-& gt;argv[2]);
}
再次查看setGenericCommand:
1 void setGenericCommand(redisClient * c,int nx,robj *key,robj *val,robj *expire) {
2長秒= 0;/*初始化以避免有害警告*/
三
4如果(過期){
5 if (getLongFromObjectOrReply(c,expire,& amp秒,NULL)!= REDIS_OK)
6返回;
7 if(秒& lt= 0) {
8 addReplyError(c,“SETEX中無效的過期時間”);
9返回;
10 }
11 }
12
13 if(lookupKeyWrite(c-& gt;db,key)!= NULL & amp& ampnx) {
14 addReply(c,shared . czero);
15返回;
16 }
17 setKey(c->;db,key,val);
18 server . dirty++;
19 if(expire)set expire(c-& gt;db,key,time(空)+秒);
20 addReply(c,nx?shared . cone:shared . ok);
21 }
22
第13行處理“設置壹個鍵的值,僅當該鍵不存在”的場景,第17行插入該鍵,第19行設置其超時。請註意,時間戳已被設置為到期時間。我們來看看redisDb(也就是C->;db的定義):
typedef結構redisDb {
dict * dict/*此數據庫的密鑰空間*/
dict *過期;/*設置了超時的鍵超時*/
dict * blocking _ keys/*客戶端等待數據的密鑰(BLPOP) */
dict * io _ keys/*客戶端等待虛擬機I/O的密鑰*/
dict * watched _ keys/* MULTI/EXEC CAS的監視鍵*/
int id
} redisDb
只關註dict和expires,分別存儲key-value和它的超時,也就是說,如果壹個key-value有超時,那麽它會存儲在dict中,也存儲在expires中,類似於這樣的形式:DICT [key]: value,Expires [key]: timeout。
當然,key-value沒有超時,expires中也不存在這個鍵。剩下的兩個函數,setKey和setExpire,無非就是把數據插入到兩個字典裏,這裏就不贅述了。
那麽redis如何刪除過期的密鑰呢?
通過查看dbDelete的調用者,我首先註意到這個函數用於刪除過期的密鑰。
1 int expire ifneed(redis db * db,robj *key) {
2 time_t when = getExpire(db,key);
三
4如果(當& lt0)返回0;/*該密鑰沒有過期*/
五
6 /*加載時不要讓任何東西過期。以後再做。*/
7 if (server.loading)返回0;
八
9 /*如果我們運行在從機環境中,請盡快返回:
10 *從密鑰到期由主密鑰控制,主密鑰將
11 *給我們發送過期密鑰的合成DEL操作。
12 *
13 *我們仍然試圖將正確的信息返回給呼叫者,
14 *也就是說,如果我們認為密鑰應該仍然有效,則為0,如果
15 *我們認為此時密鑰已經過期。*/
16 if (server.masterhost!= NULL) {
17返回時間(空)>當;
18 }
19
20 /*當此密鑰未過期時返回*/
21 if(time(NULL)& lt;= when)返回0;
22
23 /*刪除密鑰*/
24 server . stat _ expired keys++;
25 propagateExpire(db,key);
26返回dbDelete(db,key);
27 }
28
IfNeed表示可以刪除,所以不設置超時不刪除4行,“加載”時不刪除7行,主庫中不刪除16行,過期前不刪除21行。25行同步從庫和文件。
我們來看看哪些函數調用過期了,比如lookupKeyRead,lookupKeyWrite,dbRandomKey,existsCommand,keysCommand。通過這些函數的命名,我們可以看到,只要壹個密鑰被訪問,接下來要做的事情就是試圖查看過期的密鑰並刪除它,這就保證了用戶不可能訪問過期的密鑰。但是如果大量的密鑰過期而沒有被訪問,就會浪費大量的內存。Redis是如何處理這個問題的?
dbDelete的調用者也發現了這樣壹個函數:
1 /*嘗試終止壹些超時的密鑰。所使用的算法是自適應的
2 *將使用較少的CPU周期,否則
3 *它將變得更加激進,以避免過多的內存被
4 *可以從鑰匙槽中取出的鑰匙。*/
5 void activeExpireCycle(void) {
6 int j;
七
8 for(j = 0;j & ltserver . DBM num;j++) {
9 int過期;
10 redis db * db = server . d b+j;
11
12 /*如果在周期結束時超過25%,則繼續過期
13 *的密鑰已過期。*/
14 do {
15 long num = dictSize(d B- & gt;過期);
16 time _ t now = time(NULL);
17
18過期= 0;
19 if(num & gt;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(d B- & gt;expires))= = NULL)break;
26t =(time _ t)dictGetEntryVal(de);
27如果(現在& gtt) {
28 SDS key = dictGetEntryKey(de);
29 robj * key obj = createStringObject(key,SDS len(key));
30
31 propagateExpire(db,key obj);
32 dbDelete(db,key obj);
33 decrefcount(key obj);
34過期++;
35 server . stat _ expired keys++;
36 }
37 }
38 } while(過期& gtREDIS _ EXPIRELOOKUPS _ PER _ CRON/4);
39 }
40 }
41
這個函數的意圖已經解釋過了:刪除壹點過期密鑰,過期密鑰少的話,只需要壹點cpu。25行隨機取壹個鍵,38行刪鍵概率低就退出。這個函數放在cron中,每毫秒調用壹次。這種算法保證了每次都會刪除壹定比例的密鑰,但是如果密鑰總數大,比例控制得太多,就需要更多的周期,浪費cpu,如果控制得太少,就會有更多的過期密鑰,浪費內存——這就是時空權衡。
最後,在dbDelete的調用者中找到壹個函數:
/*當配置文件中的“maxmemory”設置為limit時,調用此函數
*服務器使用的最大內存,內存不足。
*此功能將嘗試,按順序:
*
* -從自由列表中釋放對象
* -嘗試刪除設置過期的密鑰
*
*不可能釋放足夠的內存來達到已用內存& ltmaxmemory
*服務器將開始拒絕會進壹步擴大的命令
*內存使用。
*/
void freememoryifyinded(void)
這個函數太長了,就不贅述了。評論部分解釋了這個函數只有在配置文件中設置了最大內存時才會被調用,設置這個參數的意義在於妳把redis當成了內存緩存而不是鍵值數據庫。
以上三種刪除過期密鑰的方法,第二種是定時刪除壹定比例的密鑰,第壹種是在讀取時刪除過期密鑰,第三種是在內存超過設定值時的暴力手段。也可以看出redis設計的匠心。