Redis事件

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)如果是集群模式,則對集群進行同步和連接測試

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • redis的主要事件源包括信號,網絡,文件和時間事件,文件事件沒看到用于做什么。 1、信號事件 redis忽略了S...
    x1wan閱讀 568評論 0 1
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,769評論 25 708
  • 生活是什么?生活就是吃喝拉撒?就是柴木油鹽?就是家長里短? 過了這么多年。我也不確定生活是什么。或許父母給了我們生...
    sunflowerqiu閱讀 333評論 0 1
  • 01417歐陽微 剛看到這本書名《我和我的楓姐姐》的時候,讓我想起我小時候和我妹妹小時間的一些事,到現在...
    微微姐姐閱讀 453評論 0 0
  • 我的男友是軍人, 所以沒有陪伴, 我只有思念。 我的男友是軍人, 因為戎裝在身, 他選擇報國。 我的男友是軍人, ...
    渠六億閱讀 785評論 2 7