Redis的時間事件分爲兩類:
1、定時事件:讓一段程序在指定的時間之後執行一次。
2、週期性事件:讓一段程序每隔指定的時間就執行一次。(比如serverCron函數,每秒執行次數爲server.hz)
目前版本的Redis只使用週期性事件,而沒有使用定時事件。具體源碼參考ae.c/ae.h文件中。文中的源碼註釋參考於黃建宏。
在介紹時間事件結構之前,先看錶徵事件處理器的狀態的結構:
typedef struct aeEventLoop {
// 目前已註冊的最大描述符
int maxfd; /* highest file descriptor currently registered */
// 目前已追蹤的最大描述符
int setsize; /* max number of file descriptors tracked */
// 用於生成時間事件 id
long long timeEventNextId;
// 最後一次執行時間事件的時間,用於檢測系統時鐘偏差
time_t lastTime; /* Used to detect system clock skew */
// 已註冊的文件事件
aeFileEvent *events; /* Registered events */
// 已就緒的文件事件
aeFiredEvent *fired; /* Fired events */
// 時間事件,表示時間事件頭節點
aeTimeEvent *timeEventHead;
// 事件處理器的開關
int stop;
// 多路複用庫的私有數據
void *apidata; /* This is used for polling API specific data */
// 在處理事件前要執行的函數
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
時間事件結構:
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 *next;
} aeTimeEvent;
關鍵屬性具體解釋如下:
1、id:服務器爲該時間事件創建的全局唯一標識號。新創建的時間事件的id號比舊事件大,依次遞增加1。這一點可以從事件處理器的狀態aeEventLoop中定義的變量timeEventNextId看出。
2、when_sec/when_ms:ms精度的UNIX時間域,記錄了事件的到達時間。
3、timeProc:時間事件處理器,一個函數。當時間事件到達時,服務器就會執行對應的實事件處理器,並獲取返回值。
該返回值retval來決定是否需要循環執行這個時間事件,有兩種情況:
(1)AE_NOMORE(-1):在ae.h中定義的常量值。表示該事件是一個定時事件,當該事件到達執行一次後,就會刪除。
(2)具體的ms值:表示retval毫秒後繼續執行該時間事件。
4、next:指向下一個時間事件,形成無序鏈表結構,該無序體現在並非按照到達時間進行排序。新的時間事件是插入在該鏈表的表頭位置。
這裏面有個問題:採用無序鏈表結構,遍歷執行時會不會影響時間事件處理器的性能?
答:不會。目前版本中,正常模式中Redis服務器只使用serverCron一個時間事件,在benchmark模式下,也只用兩個時間事件。在這種情況下,服務器幾乎將無序鏈表退化成一個指針來使用,所以並不影響事件執行的性能。
常用的API:
1、aeCreateTimeEvent:添加新的事件到鏈表表頭
/*
* 創建時間事件
* 返回時間事件的id
*/
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
{
// 更新時間計數器
long long id = eventLoop->timeEventNextId++;
// 創建時間事件結構
aeTimeEvent *te;
te = zmalloc(sizeof(*te));
if (te == NULL) return AE_ERR; //事件執行狀態:出錯
// 設置 ID
te->id = id;
// 設定處理事件的時間
aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
// 設置事件處理器
te->timeProc = proc;
//設置事件釋放函數
te->finalizerProc = finalizerProc;
// 設置私有數據
te->clientData = clientData;
// 將新事件放入表頭
te->next = eventLoop->timeEventHead;
eventLoop->timeEventHead = te;
return id;
}
設置到達時間的函數aeAddMillisecondsToNow源碼爲:
/*
* 在當前時間上加上 milliseconds 毫秒,
* 並且將加上之後的秒數和毫秒數分別保存在 sec 和 ms 指針中。
*/
static void aeAddMillisecondsToNow(long long milliseconds, long *sec, long *ms) {
long cur_sec, cur_ms, when_sec, when_ms;
// 獲取當前時間
aeGetTime(&cur_sec, &cur_ms);
// 計算增加 milliseconds 之後的秒數和毫秒數
when_sec = cur_sec + milliseconds/1000;
when_ms = cur_ms + milliseconds%1000;
// 進位:
// 如果 when_ms 大於等於 1000
// 那麼將 when_sec 增大一秒
if (when_ms >= 1000) {
when_sec ++;
when_ms -= 1000;
}
// 保存到指針中
*sec = when_sec;
*ms = when_ms;
}
2、aeDeleteTimeEvent:刪除給定id的時間事件
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
aeTimeEvent *te, *prev = NULL;
// 遍歷鏈表
te = eventLoop->timeEventHead;
while(te) {
// 發現目標事件,刪除
if (te->id == id) {
if (prev == NULL)
eventLoop->timeEventHead = te->next;
else
prev->next = te->next;
// 執行清理處理器
if (te->finalizerProc)
te->finalizerProc(eventLoop, te->clientData);
// 釋放時間事件
zfree(te);
return AE_OK;
}
prev = te;
te = te->next;
}
return AE_ERR; /* NO event with the specified ID found */
}
3、aeSearchNearestTimer:尋找離目前時間最近的時間事件
// 尋找裏目前時間最近的時間事件
// 因爲鏈表是亂序的,所以查找複雜度爲 O(N)
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
aeTimeEvent *te = eventLoop->timeEventHead;
aeTimeEvent *nearest = NULL;
while(te) {
if (!nearest || te->when_sec < nearest->when_sec || //遍歷頭節點時nearest爲空
(te->when_sec == nearest->when_sec &&
te->when_ms < nearest->when_ms))
nearest = te;
te = te->next;
}
return nearest;
}
4、processTimeEvents:處理所有已到達的時間事件
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te;
long long maxId;
time_t now = time(NULL);
/* If the system clock is moved to the future, and then set back to the
* right value, time events may be delayed in a random way. Often this
* means that scheduled operations will not be performed soon enough.
*
* Here we try to detect system clock skews, and force all the time
* events to be processed ASAP when this happens: the idea is that
* processing events earlier is less dangerous than delaying them
* indefinitely, and practice suggests it is. */
// 如果系統時鐘被移動到未來,然後返回到正確的值,那麼時間事件可能會以一種隨機的方式延遲。通常這意味着計劃的操作不會很快執行。
// 在這裏,我們試圖檢測系統時鐘的傾斜,並在發生這種情況時強制所有的時間事件被處理。
// 早期處理事件比無限期地延遲它們的危險要小
// 通過重置事件的運行時間,
// 防止因時間穿插(skew)而造成的事件處理混亂
if (now < eventLoop->lastTime) {
te = eventLoop->timeEventHead;
while(te) {
te->when_sec = 0;
te = te->next;
}
}
// 更新最後一次處理時間事件的時間
eventLoop->lastTime = now;
// 遍歷鏈表
// 執行那些已經到達的事件
te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId-1;
while(te) {
long now_sec, now_ms;
long long id;
// 跳過無效事件
if (te->id > maxId) {
te = te->next;
continue;
}
// 獲取當前時間
aeGetTime(&now_sec, &now_ms);
// 如果當前時間等於或等於事件的執行時間,那麼說明事件已到達,執行這個事件
if (now_sec > te->when_sec ||
(now_sec == te->when_sec && now_ms >= te->when_ms))
{
int retval;
id = te->id;
// 執行事件處理器,並獲取返回值
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
/* After an event is processed our time event list may
* no longer be the same, so we restart from head.
* Still we make sure to don't process events registered
* by event handlers itself in order to don't loop forever.
* To do so we saved the max ID we want to handle.
*
* FUTURE OPTIMIZATIONS:
* Note that this is NOT great algorithmically. Redis uses
* a single time event so it's not a problem but the right
* way to do this is to add the new elements on head, and
* to flag deleted elements in a special way for later
* deletion (putting references to the nodes to delete into
* another linked list). */
// 記錄是否有需要循環執行這個事件時間
if (retval != AE_NOMORE) {
// 是的, retval 毫秒之後繼續執行這個時間事件
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
// 不,將這個事件刪除
aeDeleteTimeEvent(eventLoop, id);
}
// 因爲執行事件之後,事件列表可能已經被改變了
// 因此需要將 te 放回表頭,繼續開始執行事件
te = eventLoop->timeEventHead;
} else {
te = te->next;
}
}
return processed;
}
通過遍歷時間事件鏈表,來處理所有已經到達的時間事件,需要注意的是:
(1)如果系統時鐘被移動到未來,那麼時間事件可能會以一種隨機的方式延遲,意味着計劃的操作不會很快被執行。所以需要用eventLoop中定義的lastTime來檢測系統時鐘的傾斜,確保發生這種情況時強制所有的時間事件被執行:te->when_sec=0。
(2)執行完具體的事件處理器後,根據返回值retval來判斷是否需要循環執行這個時間事件。
(3)執行完具體的事件後,事件鏈表可能已經被改變了,因爲需要重新迭代至表頭,繼續判斷執行事件。
事件的調度與執行:
前面提過,Redis中的事件分爲時間事件和文本事件,具體的調度源碼爲:
/* Process every pending time event, then every pending file event
* (that may be registered by time event callbacks just processed).
*
* 處理所有已到達的時間事件,以及所有已就緒的文件事件。
*
* Without special flags the function sleeps until some file event
* fires, or when the next time event occurs (if any).
*
* 如果不傳入特殊 flags 的話,那麼函數睡眠直到文件事件就緒,
* 或者下個時間事件到達(如果有的話)。
*
* If flags is 0, the function does nothing and returns.
* 如果 flags 爲 0 ,那麼函數不作動作,直接返回。
*
* if flags has AE_ALL_EVENTS set, all the kind of events are processed.
* 如果 flags 包含 AE_ALL_EVENTS ,所有類型的事件都會被處理。
*
* if flags has AE_FILE_EVENTS set, file events are processed.
* 如果 flags 包含 AE_FILE_EVENTS ,那麼處理文件事件。
*
* if flags has AE_TIME_EVENTS set, time events are processed.
* 如果 flags 包含 AE_TIME_EVENTS ,那麼處理時間事件。
*
* if flags has AE_DONT_WAIT set the function returns ASAP until all
* the events that's possible to process without to wait are processed.
* 如果 flags 包含 AE_DONT_WAIT ,
* 那麼函數在處理完所有不許阻塞的事件之後,即刻返回。
*
* The function returns the number of events processed.
* 函數的返回值爲已處理事件的數量
*/
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* Nothing to do? return ASAP */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
// 請注意,我們想要調用select,儘管沒有文件事件要處理,只要我們想處理時間事件,以便休眠,直到在下一次事件準備好被觸發。
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
// 獲取最近的時間事件
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
// 如果時間事件存在的話
// 那麼根據最近可執行時間事件和現在時間的時間差來決定文件事件的阻塞時間
long now_sec, now_ms;
/* Calculate the time missing for the nearest
* timer to fire. */
// 計算距今最近的時間事件還要多久才能達到
// 並將該時間距保存在 tv 結構中
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
tvp->tv_sec = shortest->when_sec - now_sec;
if (shortest->when_ms < now_ms) {
tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
tvp->tv_sec --;
} else {
tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
}
// 時間差小於 0 ,說明事件已經可以執行了,將秒和毫秒設爲 0 (不阻塞)
if (tvp->tv_sec < 0) tvp->tv_sec = 0;
if (tvp->tv_usec < 0) tvp->tv_usec = 0;
} else {
// 執行到這一步,說明沒有時間事件
// 那麼根據 AE_DONT_WAIT 是否設置來決定是否阻塞,以及阻塞的時間長度
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to set the timeout
* to zero */
if (flags & AE_DONT_WAIT) {
// 設置文件事件不阻塞
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
// 文件事件可以阻塞直到有事件到達爲止
tvp = NULL; /* wait forever */
}
}
// 處理文件事件,阻塞時間由 tvp 決定
// aeApiPoll() 調用了 select() 進入了監聽輪詢
// 返回已就緒的文件事件數量
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
// 從已就緒數組中獲取事件
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
// 讀事件
if (fe->mask & mask & AE_READABLE) {
// rfired 確保讀/寫事件只能執行其中一個
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
// 寫事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
processed++;
}
}
/* Check time events */
// 執行時間事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
需要解釋的是:
(1)輸入參數中的flag是決定執行什麼的事件。
0:什麼也不做,直接返回
AE_FILE_EVENTS :1,只處理文件事件
AE_TIME_EVENTS :2,只處理時間事件
AE_ALL_EVENTS:3,先處理文件事件,再處理時間事件
AE_DONT_WAIT:4,不阻塞,也不進行等待
(2)尋找距離現在最近的時間事件,先處理文件事件,aeApiPoll函數的最大阻塞時間由到達時間最接近當前時間的時間事件決定,該函數調用了select函數進入了監聽輪詢,返回已就緒的文件事件數量。接着,遍歷執行相應的讀事件或寫事件(注意,讀/寫事件只能執行其中一個)。最後,執行時間事件。