摘要:服務(wù)器會為執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器。當(dāng)服務(wù)器套接字變得可寫時(shí),套接字會產(chǎn)生事件。多路復(fù)用程序允許服務(wù)器同時(shí)監(jiān)聽套接字的事件和事件。
眾所周知,Redis 服務(wù)器是一個(gè)事件驅(qū)動程序。那么事件驅(qū)動對于 Redis 而言有什么含義?源碼中又是如何實(shí)現(xiàn)事件驅(qū)動的呢?今天,我們一起來認(rèn)識下 Redis 服務(wù)器的事件驅(qū)動。
對于 Redis 而言,服務(wù)器需要處理以下兩類事件:
文件事件(file event):Redis 服務(wù)器通過套接字與客戶端進(jìn)行連接,而文件事件就是服務(wù)器對套接字操作的抽象。服務(wù)器與客戶端的通信會產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列的網(wǎng)絡(luò)通信操作。
時(shí)間時(shí)間(time event):Redis 服務(wù)器中的一些操作(比如 serverCron 函數(shù))需要在給定的時(shí)間點(diǎn)執(zhí)行,而時(shí)間事件就是服務(wù)器對這類定時(shí)操作的抽象。
接下來,我們先來認(rèn)識下文件事件。
1 文件事件Redis 基于 Reactor 模式開發(fā)了自己的網(wǎng)絡(luò)事件處理器,這個(gè)處理器被稱為文件事件處理器(file event handler):
文件事件處理器使用 IO 多路復(fù)用程序來同時(shí)監(jiān)聽多個(gè)套接字,并根據(jù)套接字目前執(zhí)行的任務(wù)來為套接字關(guān)聯(lián)不同的事件處理器。
當(dāng)被監(jiān)聽的套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、讀?。╮ead)、寫入(write)、關(guān)閉(close)等操作時(shí),與操作相對應(yīng)的文件事件就會產(chǎn)生,這時(shí)文件事件處理器就會調(diào)用套接字之前關(guān)聯(lián)好的事件處理器來處理這些事件。
雖然文件處理器以單線程方式運(yùn)行,但通過 IO 多路復(fù)用程序監(jiān)聽多個(gè)套接字,既實(shí)現(xiàn)了高性能的網(wǎng)絡(luò)通信模型,又可以很好的與 Redis 服務(wù)器中其它同樣以單線程運(yùn)行的模塊進(jìn)行對接,保持了 Redis 內(nèi)部單線程設(shè)計(jì)的簡潔。
1.1 文件事件處理器的構(gòu)成圖 1 展示了文件事件處理器的四個(gè)組成部分:
套接字;
IO 多路復(fù)用程序;
文件事件分派器(dispatcher);
事件處理器;
文件事件是對套接字的抽象。每當(dāng)一個(gè)套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、寫入、讀取、關(guān)閉等操作時(shí),就好產(chǎn)生一個(gè)文件事件。因?yàn)橐粋€(gè)服務(wù)器通常會連接多個(gè)套接字,所以多個(gè)文件事件有可能會并發(fā)的出現(xiàn)。
而 IO 多了復(fù)用程序負(fù)責(zé)監(jiān)聽多個(gè)套接字,并向文件事件分派器分發(fā)那些產(chǎn)生事件的套接字。
盡管多個(gè)文件事件可能會并發(fā)的出現(xiàn),但 IO 多路復(fù)用程序總是會將所有產(chǎn)生事件的套接字都放到一個(gè)隊(duì)列里面,然后通過這個(gè)隊(duì)列,以有序、同步的方式,把每一個(gè)套接字傳輸給文件事件分派器。當(dāng)上一個(gè)套接字產(chǎn)生的事件被處理完畢之后(即,該套接字為事件所關(guān)聯(lián)的事件處理器執(zhí)行完畢),IO 多路復(fù)用程序才會繼續(xù)向文件事件分派器傳送下一個(gè)套接字。如圖 2 所示:
文件事件分派器接收 IO 多路復(fù)用程序傳來的套接字,并根據(jù)套接字產(chǎn)生的事件類型,調(diào)用相應(yīng)的事件處理器。
服務(wù)器會為執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器。這些處理器本質(zhì)上就是一個(gè)個(gè)函數(shù)。它們定義了某個(gè)事件發(fā)生時(shí),服務(wù)器應(yīng)該執(zhí)行的動作。
1.2 IO 多路復(fù)用程序的實(shí)現(xiàn)Redis 的 IO 多路復(fù)用程序的所有功能都是通過包裝常見的 select、epoll、evport 和 kqueue 這些 IO 多路復(fù)用函數(shù)庫來實(shí)現(xiàn)的。每個(gè) IO 多路復(fù)用函數(shù)庫在 Redis 源碼中都對應(yīng)一個(gè)多帶帶的文件,比如 ae_select.c、ae_poll.c、ae_kqueue.c 等。
由于 Redis 為每個(gè) IO 多路復(fù)用函數(shù)庫都實(shí)現(xiàn)了相同的 API,所以 IO 多路復(fù)用程序的底層實(shí)現(xiàn)是可以互換的,如圖 3 所示:
Redis 在 IO 多路復(fù)用程序的實(shí)現(xiàn)源碼中用 #include 宏定義了相應(yīng)的規(guī)則,**程序會在編譯時(shí)自動選擇系統(tǒng)中性能最高的 IO 多路復(fù)用函數(shù)庫來作為 Redis 的 IO 多路復(fù)用程序的底層實(shí)現(xiàn),這保證了 Redis 在各個(gè)平臺的兼容性和高性能。對應(yīng)源碼如下:
/* Include the best multiplexing layer supported by this system. * The following should be ordered by performances, descending. */ #ifdef HAVE_EVPORT #include "ae_evport.c" #else #ifdef HAVE_EPOLL #include "ae_epoll.c" #else #ifdef HAVE_KQUEUE #include "ae_kqueue.c" #else #include "ae_select.c" #endif #endif #endif1.3 事件的類型
IO 多路復(fù)用程序可以監(jiān)聽多個(gè)套接字的 ae.h/AE_READABLE 和 ae.h/AE_WRITABLE 事件,這兩類事件和套接字操作之間有以下對應(yīng)關(guān)系:
當(dāng)服務(wù)器套接字變得可讀時(shí),套接字會產(chǎn)生 AE_READABLE 事件。此處的套接字可讀,是指客戶端對套接字執(zhí)行 write、close 操作,或者有新的可應(yīng)答(acceptable)套接字出現(xiàn)時(shí)(客戶端對服務(wù)器的監(jiān)聽套接字執(zhí)行 connect 操作),套接字會產(chǎn)生 AE_READABLE 事件。
當(dāng)服務(wù)器套接字變得可寫時(shí),套接字會產(chǎn)生 AE_WRITABLE 事件。
IO 多路復(fù)用程序允許服務(wù)器同時(shí)監(jiān)聽套接字的 AR_READABLE 事件和 AE_WRITABLE 事件。如果一個(gè)套接字同時(shí)產(chǎn)生了兩個(gè)事件,那么文件分派器會優(yōu)先處理 AE_READABLE 事件,然后再處理 AE_WRITABLE 事件。簡單來說,如果一個(gè)套接字既可讀又可寫,那么服務(wù)器將先讀套接字,后寫套接字。
1.4 文件事件處理器Redis 為文件事件編寫了多個(gè)處理器,這些事件處理器分別用于實(shí)現(xiàn)不同的網(wǎng)絡(luò)通信需求。比如說:
為了對連接服務(wù)器的各個(gè)客戶端進(jìn)行應(yīng)答,服務(wù)器要為監(jiān)聽套接字關(guān)聯(lián)連接應(yīng)答處理器。
為了接收客戶端傳了的命令請求,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令請求處理器。
為了向客戶端返回命令執(zhí)行結(jié)果,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令回復(fù)處理器。
當(dāng)主服務(wù)器和從服務(wù)器進(jìn)行復(fù)制操作時(shí),主從服務(wù)器都需要關(guān)聯(lián)復(fù)制處理器。
在這些事件處理器中,服務(wù)器最常用的是與客戶端進(jìn)行通信的連接應(yīng)答處理器、命令請求處理器和命令回復(fù)處理器。
1)連接應(yīng)答處理器
networking.c/acceptTcpHandle 函數(shù)是 Redis 的連接應(yīng)答處理器,這個(gè)處理器用于對連接服務(wù)器監(jiān)聽套接字的客戶端進(jìn)行應(yīng)答,具體實(shí)現(xiàn)為 sys/socket.h/accept 函數(shù)的包裝。
當(dāng) Redis 服務(wù)器進(jìn)行初始化的時(shí)候,程序會將這個(gè)連接應(yīng)答處理器和服務(wù)器監(jiān)聽套接字的 AE_READABLE 事件關(guān)聯(lián)。對應(yīng)源碼如下
# server.c/initServer ... /* Create an event handler for accepting new connections in TCP and Unix * domain sockets. */ for (j = 0; j < server.ipfd_count; j++) { if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) { serverPanic( "Unrecoverable error creating server.ipfd file event."); } } ...
當(dāng)有客戶端用 sys/scoket.h/connect 函數(shù)連接服務(wù)器監(jiān)聽套接字時(shí),套接字就會產(chǎn)生 AE_READABLE 事件,引發(fā)連接應(yīng)答處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字應(yīng)答操作。如圖 4 所示:
2)命令請求處理器
networking.c/readQueryFromClient 函數(shù)是 Redis 的命令請求處理器,這個(gè)處理器負(fù)責(zé)從套接字中讀入客戶端發(fā)送的命令請求內(nèi)容,具體實(shí)現(xiàn)為 unistd.h/read 函數(shù)的包裝。
當(dāng)一個(gè)客戶端通過連接應(yīng)答處理器成功連接到服務(wù)器之后,服務(wù)器會將客戶端套接字的 AE_READABLE 事件和命令請求處理器關(guān)聯(lián)起來(networking.c/acceptCommonHandler 函數(shù))。
當(dāng)客戶端向服務(wù)器發(fā)送命令請求的時(shí)候,套接字就會產(chǎn)生 AR_READABLE 事件,引發(fā)命令請求處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字讀入操作,如圖 5 所示:
在客戶端連接服務(wù)器的整個(gè)過程中,服務(wù)器都會一直為客戶端套接字的 AE_READABLE 事件關(guān)聯(lián)命令請求處理器。
3)命令回復(fù)處理器
networking.c/sendReplToClient 函數(shù)是 Redis 的命令回復(fù)處理器,這個(gè)處理器負(fù)責(zé)將服務(wù)器執(zhí)行命令后得到的命令回復(fù)通過套接字返回給客戶端。
當(dāng)服務(wù)器有命令回復(fù)需要發(fā)給客戶端時(shí),服務(wù)器會將客戶端套接字的 AE_WRITABLE 事件和命令回復(fù)處理器關(guān)聯(lián)(networking.c/handleClientsWithPendingWrites 函數(shù))。
當(dāng)客戶端準(zhǔn)備好接收服務(wù)器傳回的命令回復(fù)時(shí),就會產(chǎn)生 AE_WRITABLE 事件,引發(fā)命令回復(fù)處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字寫入操作。如圖 6 所示:
當(dāng)命令回復(fù)發(fā)送完畢之后,服務(wù)器就會解除命令回復(fù)處理器與客戶端套接字的 AE_WRITABLE 事件的關(guān)聯(lián)。對應(yīng)源碼如下:
# networking.c/writeToClient ... if (!clientHasPendingReplies(c)) { c->sentlen = 0; # buffer 緩沖區(qū)命令回復(fù)已發(fā)送,刪除套接字和事件的關(guān)聯(lián) if (handler_installed) aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); /* Close connection after entire reply has been sent. */ if (c->flags & CLIENT_CLOSE_AFTER_REPLY) { freeClient(c); return C_ERR; } } ...1.5 客戶端與服務(wù)器連接事件
之前我們通過 debug 的形式大致認(rèn)識了客戶端與服務(wù)器的連接過程?,F(xiàn)在,我們站在文件事件的角度,再一次來追蹤 Redis 客戶端與服務(wù)器進(jìn)行連接并發(fā)送命令的整個(gè)過程,看看在過程中會產(chǎn)生什么事件,這些事件又是如何被處理的。
先來看客戶端與服務(wù)器建立連接的過程:
先啟動我們的 Redis 服務(wù)器(127.0.0.1-8379)。成功啟動后,服務(wù)器套接字(127.0.0.1-8379) AE_READABLE 事件正處于被監(jiān)聽狀態(tài),而該事件對應(yīng)連接應(yīng)答處理器。(server.c/initServer())。
使用 redis-cli 連接服務(wù)器。這是,服務(wù)器套接字(127.0.0.1-8379)將產(chǎn)生 AR_READABLE 事件,觸發(fā)連接應(yīng)答處理器執(zhí)行(networking.c/acceptTcpHandler())。
對客戶端的連接請求進(jìn)行應(yīng)答,創(chuàng)建客戶端套接字,保存客戶端狀態(tài)信息,并將客戶端套接字的 AE_READABLE 事件與命令請求處理器(networking.c/acceptCommonHandler())進(jìn)行關(guān)聯(lián),使得服務(wù)器可以接收該客戶端發(fā)來的命令請求。
此時(shí),客戶端已成功與服務(wù)器建立連接了。上述過程,我們?nèi)匀豢梢杂?gdb 調(diào)試,查看函數(shù)的執(zhí)行過程。具體調(diào)試過程如下:
gdb ./src/redis-server (gdb) b acceptCommonHandler # 給 acceptCommonHandler 函數(shù)設(shè)置斷點(diǎn) (gdb) r redis-conf --port 8379 # 啟動服務(wù)器
另外開一個(gè)窗口,使用 redis-cli 連接服務(wù)器:redis-cli -p 8379
回到服務(wù)器窗口,我們會看到已進(jìn)入 gdb 調(diào)試模式,輸入:info stack,可以看到如圖 6 所示的堆棧信息。
現(xiàn)在,我們再來認(rèn)識命令的執(zhí)行過程:
客戶端向服務(wù)器發(fā)送一個(gè)命令請求,客戶端套接字產(chǎn)生 AE_READABLE 事件,引發(fā)命令請求處理器(readQueryFromClient)執(zhí)行,讀取客戶端的命令內(nèi)容;
根據(jù)客戶端發(fā)送命令內(nèi)容,格式化客戶端 argc、argv 等相關(guān)值屬性值;
根據(jù)命令名稱查找對應(yīng)函數(shù)。server.c/processCommad() 中 lookupCommand 函數(shù)調(diào)用;
執(zhí)行與命令名關(guān)聯(lián)的函數(shù),獲得返回結(jié)果,客戶端套接字產(chǎn)生 。server.c/processCommad() 中 call 函數(shù)調(diào)用。
返回命令回復(fù),刪除客戶端套接字與 AE_WRITABLE 事件的關(guān)聯(lián)。network.c/writeToClient() 函數(shù)。
圖 7 展示了命令執(zhí)行過程的堆棧信息。圖 8 則展示了命令回復(fù)過程的堆棧信息。
上一節(jié)我們一起認(rèn)識了文件事件。接下來,讓我們再來認(rèn)識下時(shí)間事件。
2 時(shí)間事件Redis 的時(shí)間時(shí)間分為以下兩類:
定時(shí)時(shí)間:讓一段程序在指定的時(shí)間之后執(zhí)行一次。比如,讓程序 M 在當(dāng)前時(shí)間的 60 毫秒后執(zhí)行一次。
周期性事件:讓一段程序每隔指定時(shí)間就執(zhí)行一次。比如,讓程序 N 每隔 30 毫秒執(zhí)行一次。
對于時(shí)間事件,數(shù)據(jù)結(jié)構(gòu)源碼(ae.h/aeTimeEvent):
/* 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 *next; } aeTimeEvent;
主要屬性說明:
id:服務(wù)器為時(shí)間事件創(chuàng)建的全局唯一 ID。ID 號按從小到大的順序遞增。
when_sec:秒精度的 UNIX 時(shí)間戳,記錄了時(shí)間事件的到達(dá)時(shí)間。
when_ms:毫秒精度的 UNIX 時(shí)間戳,記錄了時(shí)間事件的到達(dá)時(shí)間。
timeProc:時(shí)間事件處理器,對應(yīng)一個(gè)函數(shù)。當(dāng)時(shí)間事件發(fā)生時(shí),服務(wù)器就會調(diào)用相應(yīng)的處理器來處理事件。
時(shí)間事件進(jìn)程執(zhí)行的函數(shù)為 ae.c/processTimeEvents()。
此外,對于時(shí)間事件的類型區(qū)分,取決于時(shí)間事件處理器的返回值:
返回值是 ae.h/AE_NOMORE,為定時(shí)事件。該事件在到達(dá)一次后就會被刪除;
返回值不是 ae.h/AE_NOMORE,為周期事件。當(dāng)一個(gè)周期時(shí)間事件到達(dá)后,服務(wù)器會根據(jù)事件處理器返回的值,對時(shí)間事件的 when_sec 和 when_ms 屬性進(jìn)行更新,讓這個(gè)事件在一段時(shí)間之后再次到達(dá),并以這種方式一致更新運(yùn)行。比如,如果一個(gè)時(shí)間事件處理器返回 30,那么服務(wù)器應(yīng)該對這個(gè)時(shí)間事件進(jìn)行更新,讓這個(gè)事件在 30 毫秒后再次執(zhí)行。
2.1 時(shí)間事件之 serverCron 函數(shù)持續(xù)運(yùn)行的 Redis 服務(wù)器需要定期對自身的資源和狀態(tài)進(jìn)行檢查和調(diào)整,從而確保服務(wù)可以長期、穩(wěn)定的運(yùn)行。這些定期操作由 server.c/serverCron() 函數(shù)負(fù)責(zé)執(zhí)行。主要操作包括:
更新服務(wù)器的各類統(tǒng)計(jì)信息。比如時(shí)間、內(nèi)存占用、數(shù)據(jù)庫占用情況等。
清理數(shù)據(jù)庫中的過期鍵值對。
關(guān)閉和清理連接失效的客戶端。
嘗試進(jìn)行 AOF 或 RDB 持久化操作。
如果服務(wù)器是主服務(wù)器,對從 服務(wù)器進(jìn)行定期同步。
如果處于集群模式,對集群進(jìn)行定期同步和連接測試。
Redis 服務(wù)器以周期性事件的方式來運(yùn)行 serverCron 函數(shù),在服務(wù)器運(yùn)行期間,每隔一段時(shí)間,serverCron 就會執(zhí)行一次,直到服務(wù)器關(guān)閉為止。
關(guān)于執(zhí)行次數(shù),可參見 redis.conf 文件中的 hz 選項(xiàng)。默認(rèn)為 10,表示每秒運(yùn)行 10 次。
3 事件調(diào)度與執(zhí)行由于服務(wù)器同時(shí)存在文件事件和時(shí)間事件,所以服務(wù)器必須對這兩種事件進(jìn)行調(diào)度,來決定何時(shí)處理文件事件,何時(shí)處理時(shí)間事件,以及花多少時(shí)間來處理它們等等。
事件的調(diào)度和執(zhí)行有 ae.c/aeProcessEvents() 函數(shù)負(fù)責(zé)。源碼如下:
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; /* 首先判斷是否存在需要監(jiān)聽的文件事件,如果存在需要監(jiān)聽的文件事件,那么通過IO多路復(fù)用程序獲取 * 準(zhǔn)備就緒的文件事件,至于IO多路復(fù)用程序是否等待以及等待多久的時(shí)間,依發(fā)生時(shí)間距離現(xiàn)在最近的時(shí)間事件確定; * 如果eventLoop->maxfd == -1表示沒有需要監(jiān)聽的文件事件,但是時(shí)間事件肯定是存在的(serverCron()), * 如果此時(shí)沒有設(shè)置 AE_DONT_WAIT 標(biāo)志位,此時(shí)調(diào)用IO多路復(fù)用,其目的不是為了監(jiān)聽文件事件是否準(zhǔn)備就緒, * 而是為了使線程休眠到發(fā)生時(shí)間距離現(xiàn)在最近的時(shí)間事件的發(fā)生時(shí)間(作用類似于unix中的sleep函數(shù)), * 這種休眠操作的目的是為了避免線程一直不停的遍歷時(shí)間事件形成的無序鏈表,造成不必要的資源浪費(fèi) */ if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { int j; aeTimeEvent *shortest = NULL; struct timeval tv, *tvp; /* 尋找發(fā)生時(shí)間距離現(xiàn)在最近的時(shí)間事件,該時(shí)間事件的發(fā)生時(shí)間與當(dāng)前時(shí)間之差就是IO多路復(fù)用程序應(yīng)該等待的時(shí)間 */ if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT)) shortest = aeSearchNearestTimer(eventLoop); if (shortest) { long now_sec, now_ms; // 創(chuàng)建 timeval 結(jié)構(gòu) 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; /* 如果時(shí)間之差大于0,說明時(shí)間事件到時(shí)時(shí)間未到,則等待對應(yīng)的時(shí)間; * 如果時(shí)間間隔小于0,說明時(shí)間事件已經(jīng)到時(shí),此時(shí)如果沒有 * 文件事件準(zhǔn)備就緒,那么IO多路復(fù)用程序應(yīng)該立即返回,以免 * 耽誤處理時(shí)間事件*/ if (ms > 0) { tvp->tv_sec = ms/1000; tvp->tv_usec = (ms % 1000)*1000; } else { tvp->tv_sec = 0; tvp->tv_usec = 0; } } else { /* 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 */ } } // 阻塞并等等文件事件產(chǎn)生,最大阻塞事件由 timeval 結(jié)構(gòu)決定 numevents = aeApiPoll(eventLoop, tvp); for (j = 0; j < numevents; j++) { // 處理所有已產(chǎn)生的文件事件 aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; int mask = eventLoop->fired[j].mask; int fd = eventLoop->fired[j].fd; int fired = 0; /* Number of events fired for current fd. */ int invert = fe->mask & AE_BARRIER; if (!invert && fe->mask & mask & AE_READABLE) { fe->rfileProc(eventLoop,fd,fe->clientData,mask); fired++; } /* Fire the writable event. */ if (fe->mask & mask & AE_WRITABLE) { if (!fired || fe->wfileProc != fe->rfileProc) { fe->wfileProc(eventLoop,fd,fe->clientData,mask); fired++; } } /* If we have to invert the call, fire the readable event now * after the writable one. */ if (invert && fe->mask & mask & AE_READABLE) { if (!fired || fe->wfileProc != fe->rfileProc) { fe->rfileProc(eventLoop,fd,fe->clientData,mask); fired++; } } processed++; } } /* Check time events */ if (flags & AE_TIME_EVENTS) // 處理所有已到達(dá)的時(shí)間事件 processed += processTimeEvents(eventLoop); return processed; /* return the number of processed file/time events */ }
將 aeProcessEvents 函數(shù)置于一個(gè)循環(huán)里面,加上初始化和清理函數(shù),就構(gòu)成了 Redis 服務(wù)器的主函數(shù) server.c/main()。以下是主函數(shù)的偽代碼:
def main(): // 初始化服務(wù)器 init_server(); // 一直處理事件,直到服務(wù)器關(guān)閉為止 while server_is_not_shutdown(): aeProcessEvents(); // 服務(wù)器關(guān)閉,執(zhí)行清理操作 clear_server()
從事件處理的角度來看,Redis 服務(wù)器的運(yùn)行流程可以用流程圖 1 來概括:
以下是事件的調(diào)度和執(zhí)行規(guī)則:
aeApiPoll 函數(shù)的最大阻塞事件由到達(dá)時(shí)間最接近當(dāng)前時(shí)間的時(shí)間事件決定。這個(gè)方法既可以避免服務(wù)器對時(shí)間事件進(jìn)行頻繁的輪詢,也可以確保 aeApiPoll 函數(shù)不會阻塞過長時(shí)間。
因?yàn)槲募录请S機(jī)出現(xiàn)的,如果等待并處理完一次文件事件之后,仍未有任何時(shí)間事件到達(dá),那么服務(wù)器將再次等待并處理文件事件。隨著文件事件的不斷執(zhí)行,時(shí)間會逐漸向時(shí)間事件所設(shè)置的到達(dá)時(shí)間逼近,并最終來到,這時(shí)服務(wù)器就可以開始處理到達(dá)的時(shí)間事件了。
對文件事件和時(shí)間事件的處理都是同步、有序、原子地執(zhí)行。服務(wù)器不會中途中斷事件處理,也不會對事件進(jìn)行搶占。因此,不管是文件事件的處理器,還是時(shí)間事件的處理器,它們斗毆盡可能的減少程序的阻塞事件,并在有需要時(shí)主動讓出執(zhí)行權(quán),從而降低事件饑餓的可能性。舉個(gè)栗子,在命令回復(fù)處理器將一個(gè)命令回復(fù)寫入到客戶端套接字時(shí),如果寫入字節(jié)數(shù)超過了一個(gè)預(yù)設(shè)常量,命令回復(fù)處理器就會主動用 break 跳出寫入循環(huán),將余下的數(shù)據(jù)留到下次再寫。另外,時(shí)間事件也會將非常耗時(shí)的持久化操作放到子線程或者子進(jìn)程中執(zhí)行。
因?yàn)闀r(shí)間事件在文件事件之后執(zhí)行,并且事件之間不會出現(xiàn)搶占,所以時(shí)間事件的實(shí)際處理時(shí)間,通常會比時(shí)間事件設(shè)定的時(shí)間稍晚一些。
總結(jié)Redis 服務(wù)器是一個(gè)事件驅(qū)動程序,服務(wù)器處理的事件分為時(shí)間事件和文件事件兩類。
文件事件是對套接字操作的抽象。**每次套接字變得可應(yīng)答(acceptable)、可寫(writable)或者可讀(readable)時(shí),相應(yīng)的文件事件就會產(chǎn)生。
文件事件分為 AE_READABLE 事件(讀事件)和 AE_WRITABLE 事件(寫事件)兩類。
時(shí)間事件分為定時(shí)事件和周期事件。定時(shí)事件只在指定時(shí)間執(zhí)行一次,而周期事件則每隔指定時(shí)間執(zhí)行一次。
服務(wù)器一般情況下只執(zhí)行 serverCron 函數(shù)這一個(gè)周期性時(shí)間事件。
時(shí)間事件和文件事件之間是合作關(guān)系。服務(wù)器會輪流處理這兩種事件,并且處理事件的過程中不會進(jìn)行搶占。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/62141.html
摘要:此時(shí)服務(wù)器處于休眠狀態(tài),并使用進(jìn)行事件輪詢,等待監(jiān)聽事件的發(fā)生。繼續(xù)執(zhí)行被調(diào)試程序,直至下一個(gè)斷點(diǎn)或程序結(jié)束縮寫。服務(wù)啟動包括初始化基礎(chǔ)配置數(shù)據(jù)結(jié)構(gòu)對外提供服務(wù)的準(zhǔn)備工作還原數(shù)據(jù)庫執(zhí)行事件循環(huán)等。 一直很羨慕那些能讀 Redis 源碼的童鞋,也一直想自己解讀一遍,但迫于 C 大魔王的壓力,解讀日期遙遙無期。 相信很多小伙伴應(yīng)該也都對或曾對源碼感興趣,但一來覺得自己不會 C 語言,二來也...
摘要:對象源碼結(jié)構(gòu)如下對象類型對象編碼引用統(tǒng)計(jì)指向底層實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針字段對象類型,就是我們常說的。。對象編碼對應(yīng)跳躍表壓縮列表集合動態(tài)字符串等八種底層數(shù)據(jù)結(jié)構(gòu)。 相信很多人應(yīng)該都知道 Redis 有五種數(shù)據(jù)類型:字符串、列表、哈希、集合和有序集合。但這五種數(shù)據(jù)類型是什么含義?Redis 的數(shù)據(jù)又是怎樣存儲的?今天我們一起來認(rèn)識下 Redis 這五種數(shù)據(jù)結(jié)構(gòu)的含義及其底層實(shí)現(xiàn)。 首先要明確...
閱讀 2580·2023-04-25 17:33
閱讀 659·2021-11-23 09:51
閱讀 2967·2021-07-30 15:32
閱讀 1413·2019-08-29 18:40
閱讀 1957·2019-08-28 18:19
閱讀 1476·2019-08-26 13:48
閱讀 2253·2019-08-23 16:48
閱讀 2285·2019-08-23 15:56