redis服務器是一個事件驅動型的,主要包括以下兩種類型的事件:
(1)文件事件:客戶端與服務器的socket連接,讀命令,寫命令都是文件事件。redis服務器是單線程,采用I/O多路復用來處理多個客戶端的請求。
(2)時間事件:周期性地執行一些操作。
1、事件循環
事件循環的核心部分是aeEventLoop,下圖為數據結構:
aeEventLoop數據結構
aeEventLoop保存了待處理的文件事件,時間事件,以及事件執行的上下文。內部持有三個事件數組:
(1)aeFileEvent *events 已注冊文件事件數組;
(2)aeFiredEvent *fired 已就緒文件事件數組;
(3)aeTimeEvent *timeEventHead 時間事件列表;
事件循環
在server啟動過程中,會調用aeMain啟動事件處理循環。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件處理前執行的函數,那么運行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 開始處理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
aeMain循環處理事件,直到eventLoop→stop=true為止。實際處理文件事件和時間事件的過程是在aeProcessEvents中。
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;
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 (flags & AE_DONT_WAIT) {
// 設置文件事件不阻塞
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
// 文件事件可以阻塞直到有事件到達為止
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;
// 讀事件
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)找出最近的時間事件,計算出文件事件的阻塞時間。
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
如果最近的時間事件存在,則根據離當前時間的時間差得出文件事件的阻塞時間;
如果不存在,則根據 AE_DONT_WAIT 是否設置來決定是否阻塞,以及阻塞的時間長度。
(2)阻塞等待就緒的文件事件
// 處理文件事件,阻塞時間由 tvp 決定
numevents = aeApiPoll(eventLoop, tvp);
底層有四種實現方式:(1)evport(2)epoll(3)kqueue(4)select
(3)處理已就緒的文件事件
第二步獲取的已就緒事件存儲在fired中。如果文件事件綁定了讀/寫事件,進行相應的處理。
// 讀事件
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);
}
其中rfileProc和wfileProc就是在文件事件被創建時傳入的函數指針。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
//省略部分代碼
// 設置文件事件類型,以及事件的處理器
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
// 私有數據
fe->clientData = clientData;
// 如果有需要,更新事件處理器的最大 fd
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
(4)執行時間事件
事件循環的流程如下圖:
事件循環過程
2、文件事件
下面來看下aeFileEvent內部結構:
typedef struct aeFileEvent {
// 監聽事件類型掩碼,
// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
int mask; /* one of AE_(READABLE|WRITABLE) */
// 讀事件處理器
aeFileProc *rfileProc;
// 寫事件處理器
aeFileProc *wfileProc;
// 多路復用庫的私有數據
void *clientData;
} aeFileEvent;
(1)共有兩種類型的文件事件:AE_READABLE和AE_WRITABLE類型。
(2)兩個函數指針:一個是處理讀事件的函數指針,一個是處理寫事件的函數指針。
創建文件事件
以下三種場景會創建文件事件:
1、當有client申請socket連接時,會注冊一個AE_READABLE類型的文件事件。
2、當接受client命令請求時,會注冊一個AE_READABLE類型的文件事件。
3、當返回命令處理結果時,會注冊一個AE_WRITABLE類型的文件事件。
(1)socket連接
創建一個AE_READABLE類型的文件事件,并注冊事件處理函數指針acceptTcpHandler
// 為 TCP 連接關聯連接應答(accept)處理器
// 用于接受并應答客戶端的 connect() 調用
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
redisPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
(2)接受client命令請求
創建Readable類型的時間,并注冊事件處理函數readQueryFromClient
// 綁定讀事件到事件 loop (開始接收命令請求)
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
(3)返回命令處理結果
創建Writable類型的文件事件,并注冊事件處理函數sendReplyToClient
int prepareClientToWrite(redisClient *c) {
...
// 一般情況,為客戶端套接字安裝寫處理器到事件循環
if (c->bufpos == 0 && listLength(c->reply) == 0 &&
(c->replstate == REDIS_REPL_NONE ||
c->replstate == REDIS_REPL_ONLINE) &&
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c) == AE_ERR) return REDIS_ERR;
return REDIS_OK;
}
3、時間事件
3.1、processTimeEvents中處理時間事件
static int processTimeEvents(aeEventLoop *eventLoop) {
...
// 通過重置事件的運行時間,防止因系統時間被修改而造成的事件處理混亂
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++;
// 記錄是否有需要循環執行這個事件時間
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)首先判斷系統時間是否被重新設置過
如果系統時間先被設置成了未來的時間,又調成到正確的時間時,為了防止部分事件延遲執行,這里會強制執行所有的時間事件。
(2)判斷時間事件是否已到達
如果當前時間大于或等于事件的執行時間,說明事件已到達,則執行事件。事件的執行由創建時間事件時傳入的函數指針te->timeProc負責。
server啟動時會注冊一個時間事件,并傳入事件處理函數serverCron
(3)判斷該時間事件是否需要循環執行
timeProc函數的返回值retval為時間事件執行的時間間隔
如果retval != AE_MORE,則修改當前事件下次執行時間,并在retval間隔之后再次執行。
如果retval == AE_MORE,則刪除當前事件。
3.2、ServerCron
serverCron是時間事件的具體執行函數,具體工作主要有:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
/*更新server的統計信息 */
updateCachedTime();
// 記錄服務器執行命令的次數
run_with_period(100) trackOperationsPerSecond();
server.lruclock = getLRUClock();
if (zmalloc_used_memory() > server.stat_peak_memory)
server.stat_peak_memory = zmalloc_used_memory();
/* Sample the RSS here since this is a relatively slow call. */
server.resident_set_size = zmalloc_get_rss();
// 檢查客戶端,關閉超時客戶端,并釋放客戶端多余的緩沖區
clientsCron();
// 對數據庫執行各種操作
databasesCron();
// 如果 BGSAVE 和 BGREWRITEAOF 都沒有在執行
// 并且有一個 BGREWRITEAOF 在等待,那么執行 BGREWRITEAOF
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
// 檢查 BGSAVE 或者 BGREWRITEAOF 是否已經執行完畢
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
...
} else {
// 遍歷所有保存條件,看是否需要執行 BGSAVE 命令
for (j = 0; j < server.saveparamslen; j++) {
...
}
/* Trigger an AOF rewrite if needed */
if (server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
// AOF 文件的當前大小大于執行 BGREWRITEAOF 所需的最小大小
server.aof_current_size > server.aof_rewrite_min_size)
{
...
}
}
/* AOF postponed flush: Try at every cron cycle if the slow fsync
* completed. */
if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
//關閉那些需要異步關閉的客戶端
freeClientsInAsyncFreeQueue();
/* Clear the paused clients flag if needed. */
clientsArePaused(); /* Don't check return value, just use the side effect. */
// 重連接主服務器、向主服務器發送 ACK 、判斷數據發送失敗情況、斷開本服務器超時的從服務器,等等
run_with_period(1000) replicationCron();
/* Run the Redis Cluster cron. */
// 如果服務器運行在集群模式下,那么執行集群操作
run_with_period(100) {
if (server.cluster_enabled) clusterCron();
}
/* Run the Sentinel timer if we are in sentinel mode. */
// 如果服務器運行在 sentinel 模式下,那么執行 SENTINEL 的主函數
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer();
}
....
return 1000/server.hz;
}
(1)更新server的統計信息
(2)關閉已斷開連接的client,并釋放client占用的空間
(3)刪除數據庫過期鍵
(4)執行AOF或RDB持久化操作
(5)進行主從服務器同步
(6)如果是集群模式,則對集群進行同步和連接測試