自頂向下redis4.0(4)時間事件與expire
阿新 • • 發佈:2020-12-15
# 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