介紹
Redis實現了自己的一套和libevent類似的事件驅動機制,主要用於處理時間事件和檔案事件。檔案事件底層主要是指網路IO事件的處理,底層使用的可能是select,epoll,或者是kqueue。Redis使用自己實現的AE而不是像memcache使用的libevent使得其效能更好,因為libevent為了其通用性增加了很多擴充功能顯然會降低使用它的效能。
源碼
ae.h ae.c
分析總體流程
簡單說來Redis使用的事件處理機制就是通過一個主aeMain迴圈在單線程執行,在每一次迴圈中首先查看是否需要有其他阻塞的用戶端或者是aof需要執行。然後在具體的aeProcessEvents中則根據傳遞的參數判斷如何處理檔案事件和時間事件。具體可以參考下面的說明。
主要函數說明aeProcessEvents
/* 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 occurrs (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. */ 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 se 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 決定 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 = 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 */}
說明:通過使用不同的flag標記來使其執行不同的操作。需要注意的是在處理檔案事件時的處理方式:
如果有處於監控中的檔案fd則直接根據傳入的flag是否等待來決定是否阻塞。如果不需要等待則aepoll不阻塞,如果需要等待則aepoll阻塞直到有事件發生。
如果沒有處於監控中的fd則判斷是否需要處理時間事件如果需要則根據最近可執行時間事件和現在時間的時間差來決定檔案事件的阻塞時間。
prcessTimeEvents
/* Process time events * * 處理所有已到達的時間事件 */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;}
說明:
在處理完一個事件以後我們的事件列表可能產生了變化,因而我們需要從頭開始再遍曆處理。
同時為了確保我們下一次處理的事件不是由當前處理的事件所註冊的,我們通過儲存當前處理的handle作為最大的handle來避免。
總結
對於Ae事件處理的理解主要依賴於對基於事件的非同步編程架構的理解,底層的關於網路poll的會在後面的文章中進一步的說明。