1. 程式人生 > >自頂向下redis4.0(4)時間事件與expire

自頂向下redis4.0(4)時間事件與expire

# redis4.0的時間事件與expire [toc] ## 簡介 時間事件和檔案事件有著相似的介面,他們都在`aeProcessEvents`中被呼叫。不同的是檔案事件底層委託給 `select`,`epoll`等多路複用介面。而時間事件通過每個tick檢查時間事件的觸發時間是否已經到期。`redis`4.0版本中只註冊了一個時間事件`serverCron`,它在`initServer`中註冊,在每次`aeProcessEvents`函式末尾被呼叫。上文已經提到`aeMain`函式是`redis`的事件主迴圈,它會不斷地呼叫`aeProcessEvents`。 `expire`指令在`server->expires`字典`dict`中插入`sds`內部資料型別的key值和到期時間,並觸發鍵空間事件。在`serverCron`中的`databaseCron`函式中呼叫`activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW)`隨機抽取`expires`中的鍵值,如果過期,則在`server->dict`中刪除對應的鍵值。 ## 正文 ### 時間事件註冊 首先我們觀察一下時間事件的結構體,雖然結構體中有許多成員,但可以說實際用到的就`when_sec` ,`when_ms`,`timeProc`3個成員還有`timeProc`的返回值。我們可以觀察到`aeTimeProc`會返回一個`int`型別的值,如果不為-1,會作為下次呼叫的間隔時間。 ```c /* Time event structure */ typedef struct aeTimeEvent { long long id; /* time event identifier. */ long when_sec; /* seconds */ long when_ms; /* milliseconds */ aeTimeProc *timeProc; aeEventFinalizerProc *finalizerProc; void *clientData; struct aeTimeEvent *prev; struct aeTimeEvent *next; } aeTimeEvent; ``` ```c typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData); ``` 註冊的函式位於`initServer`中,`aeCreateTimeEvent`函式會生成一個`aeTimeEvent`物件,並將其賦值給`eventLoop->timeEventHead`。 ```c if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { serverPanic("Can't create event loop timers."); exit(1); } ``` ### 時間事件觸發 真正處理時間事件的函式是`processTimeEvents`,但我們回到`aeProcessEvents`中學習`redis`中的一個小技巧。`aeSearchNearestTimer`會找到距離最近的時間事件。如果有(正常情況下肯定會有一個`serverCron`函式),那麼會將距離下一次時間事件的間隔事件寫入`tvp`引數,在`aeApiPoll`引數中會傳入`tvp`,如果一直沒有檔案事件觸發,那麼`aeApiPoll`函式會等待恰當的時間返回,函式返回後剛好可以處理時間事件。 ```C int aeProcessEvents(aeEventLoop *eventLoop, int flags) { int processed = 0, numevents; int j; aeTimeEvent *shortest = NULL; struct timeval tv, *tvp; shortest = aeSearchNearestTimer(eventLoop); if (shortest) { long now_sec, now_ms; aeGetTime(&now_sec, &now_ms); tvp = &tv; /* How many milliseconds we need to wait for the next * time event to fire? */ long long ms = (shortest->when_sec - now_sec)*1000 + shortest->when_ms - now_ms; if (ms > 0) { tvp->tv_sec = ms/1000; tvp->tv_usec = (ms % 1000)*1000; } else { tvp->tv_sec = 0; tvp->tv_usec = 0; } } numevents = aeApiPoll(eventLoop, tvp); for (j = 0; j < numevents; j++) { //process file events } /* Check time events */ if (flags & AE_TIME_EVENTS) processed += processTimeEvents(eventLoop); return processed; /* return the number of processed file/time events */ } ``` 在`processTimeEvents`函式中,會遍歷之前註冊的函式,如果時間條件滿足,則會呼叫對應的函式。如果函式返回的值不是`-1`,意味著函式將會利用返回值作為下一次呼叫函式的間隔時間。`ServerCron`的頻率定義在`server.hz`,表示一秒鐘呼叫幾次`serverCron`函式,預設是10次/秒。 ### expire命令 在瞭解`expire`命令之前,我們先回顧一下前文的內容,在 檔案事件處理過程中,`redis`會將`querybuf`中的內容轉化為`client->argc`和`client->argv`,方式是通過`createStringObject`轉化為對應的字串型別的物件,因此,`argv`中`redisObject`的編碼型別只可能是`embstr`或者是`raw`。 ```c #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 robj *createStringObject(const char *ptr, size_t len) { if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); } ``` 如果傳遞的`time to live`引數是負數,那麼`exipre`指令會被轉化為`del`指令,直接刪除對應的鍵值。 否則在`server->expire`內部資料型別`dict`中新增對應的到期時間。 ```c void expireGenericCommand(client *c, long long basetime, int unit) { robj *key = c->argv[1], *param = c->argv[2]; long long when; /* unix time in milliseconds when the key will expire. */ if (getLongLongFromObjectOrReply(c, param, &when, NULL) != C_OK) return; if (unit == UNIT_SECONDS) when *= 1000; when += basetime; /* No key, return zero. */ if (lookupKeyWrite(c->db,key) == NULL) { addReply(c,shared.czero); return; } if (when <= mstime() && !server.loading && !server.masterhost) { robj *aux; int deleted = dbSyncDelete(c->db,key); serverAssertWithInfo(c,key,deleted); server.dirty++; aux = shared.del; rewriteClientCommandVector(c,2,aux,key); signalModifiedKey(c->db,key); notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id); addReply(c, shared.cone); return; } else { setExpire(c,c->db,key,when); addReply(c,shared.cone); signalModifiedKey(c->db,key); notifyKeyspaceEvent(NOTIFY_GENERIC,"expire",key,c->db->id); server.dirty++; return; } } ``` ### 刪除過期鍵值 刪除過期鍵值的方式有3種:定時刪除,定期刪除,被動刪除。`redis`結合使用了定期刪除和被動刪除。 #### 被動刪除 在客戶端向服務端傳送`get` ,`expire`等請求時,會呼叫`expireIfNeeded(c->db,c->argv[j]);`函式刪除過期的鍵值。令人好奇的是`del`請求也會呼叫`expireIfNeeded`,也就是有可能呼叫2次`dbSyncDelete`函式。 #### 主動刪除/定期刪除 在前文提到的時間事件`serverCron`函式中,如果不是從庫並且開啟了`active_expire_enabled`(預設開啟),則會呼叫`activeExpireCycle`函式主動清理過期的鍵值。 預設情況下,`CRON_DBS_PER_CALL`的值為`16`,也是`dbnum`的值,意味著`activeExpireCycle`一次會處理`16`個數據庫。而且如果上次呼叫超時,也會按照一次處理`dbnum`的資料庫處理。 並且對每個資料庫**至少**會進行一輪處理,一輪處理中抽取20個樣本,如果樣本過期,則刪除該鍵。而且如果樣本中過期的鍵超過`25%`並且沒有超時,則會繼續迭代,再進行一輪處理。 `timelimit`的單位是微秒,如果對當前`db`處理的過程中超時,那麼處理之後的`db`只進行一輪處理。 所以定期刪除並不會將所有的過期鍵刪除,在伺服器正常執行的情況下,過期鍵會維持在`25%`以內。 ```c void activeExpireCycle(int type) { //靜態全域性變數 static unsigned int current_db = 0; /* Last DB tested. */ static int timelimit_exit = 0; /* Time limit hit in previous call? */ int j, iteration = 0; int dbs_per_call = CRON_DBS_PER_CALL; long long start = ustime(), timelimit; //一次處理多少db if (dbs_per_call > server.dbnum || timelimit_exit) dbs_per_call = server.dbnum; //時間限制,如果總的時間超過限制,則只處理一輪當前的db timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; timelimit_exit = 0; if (timelimit <= 0) timelimit = 1; if (type == ACTIVE_EXPIRE_CYCLE_FAST) timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */ for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { int expired; redisDb *db = server.db+(current_db % server.dbnum); current_db++; do { unsigned long num, slots; long long now, ttl_sum; int ttl_samples; iteration++; if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; break; } slots = dictSlots(db->expires); now = mstime(); expired = 0; if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; while (num--) { dictEntry *de; if ((de = dictGetRandomKey(db->expires)) == NULL) break; if (activeExpireCycleTryExpire(db,de,now)) expired++; } total_ += expired; if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */ elapsed = ustime()-start; if (elapsed > timelimit) { timelimit_exit = 1; server.stat_expired_time_cap_reached_count++; break; } } } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } } ``` ## 參考文獻 [redis 文件](https://github.com/dewxin/redis) [《Redis設計與實現》](https://book.douban.com/subject/25