Redis服務(wù)器是一個(gè)事件驅(qū)動(dòng)程序,服務(wù)器需要處理以下兩類事件:
■ 文件事件( le event):Redis服務(wù)器通過(guò)套接字與客戶端(或者其他Redis服務(wù)器)進(jìn)行連接,而文件事件就是服務(wù)器對(duì)套接字操作的抽象。服務(wù)器與客戶端(或者其他服務(wù)器)的通信會(huì)產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過(guò)監(jiān)聽(tīng)并處理這些事件來(lái)完成一系列網(wǎng)絡(luò)通信操作。
■ 時(shí)間事件(time event):Redis服務(wù)器中的一些操作(比如serverCron函數(shù))需要在給定的時(shí)間點(diǎn)執(zhí)行,而時(shí)間事件就是服務(wù)器對(duì)這類定時(shí)操作的抽象。
12.1 文件事件
Redis基于Reactor模式開(kāi)發(fā)了自己的網(wǎng)絡(luò)事件處理器:這個(gè)處理器被稱為文件事件處理器( le event handler):
■ 文件事件處理器使用I/O多路復(fù)用(multiplexing)程序來(lái)同時(shí)監(jiān)聽(tīng)多個(gè)套接字,并根據(jù)套接字目前執(zhí)行的任務(wù)來(lái)為套接字關(guān)聯(lián)不同的事件處理器。
■ 當(dāng)被監(jiān)聽(tīng)的套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、讀?。╮ead)、寫(xiě)入(write)、關(guān)閉(close)等操作時(shí),與操作相對(duì)應(yīng)的文件事件就會(huì)產(chǎn)生,這時(shí)文件事件處理器就會(huì)調(diào)用套接字之前關(guān)聯(lián)好的事件處理器來(lái)處理這些事件。
雖然文件事件處理器以單線程方式運(yùn)行,但通過(guò)使用I/O多路復(fù)用程序來(lái)監(jiān)聽(tīng)多個(gè)套接字,文件事件處理器既實(shí)現(xiàn)了高性能的網(wǎng)絡(luò)通信模型,又可以很好地與Redis服務(wù)器中其他同樣以單線程方式運(yùn)行的模塊進(jìn)行對(duì)接,這保持了Redis內(nèi)部單線程設(shè)計(jì)的簡(jiǎn)單性。
12.1.1 文件事件處理器的構(gòu)成
圖12-1展示了文件事件處理器的四個(gè)組成部分,它們分別是套接字、I/O多路復(fù)用程序、文件事件分派器(dispatcher),以及事件處理器。

文件事件是對(duì)套接字操作的抽象,每當(dāng)一個(gè)套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、寫(xiě)入、讀取、關(guān)閉等操作時(shí),就會(huì)產(chǎn)生一個(gè)文件事件。因?yàn)橐粋€(gè)服務(wù)器通常會(huì)連接多個(gè)套接字,所以多個(gè)文件事件有可能會(huì)并發(fā)地出現(xiàn)。
I/O多路復(fù)用程序負(fù)責(zé)監(jiān)聽(tīng)多個(gè)套接字,并向文件事件分派器傳送那些產(chǎn)生了事件的套接字。
盡管多個(gè)文件事件可能會(huì)并發(fā)地出現(xiàn),但I(xiàn)/O多路復(fù)用程序總是會(huì)將所有產(chǎn)生事件的套接字都放到一個(gè)隊(duì)列里面,然后通過(guò)這個(gè)隊(duì)列,以有序(sequentially)、同步(synchronously)、每次一個(gè)套接字的方式向文件事件分派器傳送套接字。當(dāng)上一個(gè)套接字產(chǎn)生的事件被處理完畢之后(該套接字為事件所關(guān)聯(lián)的事件處理器執(zhí)行完畢),I/O多路復(fù)用程序才會(huì)繼續(xù)向文件事件分派器傳送下一個(gè)套接字,如圖12-2所示。

文件事件分派器接收I/O多路復(fù)用程序傳來(lái)的套接字,并根據(jù)套接字產(chǎn)生的事件的類型,調(diào)用相應(yīng)的事件處理器。
服務(wù)器會(huì)為執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器,這些處理器是一個(gè)個(gè)函數(shù),它們定義了某個(gè)事件發(fā)生時(shí),服務(wù)器應(yīng)該執(zhí)行的動(dòng)作。
12.1.2 I/O多路復(fù)用程序的實(shí)現(xiàn)
Redis的I/O多路復(fù)用程序的所有功能都是通過(guò)包裝常見(jiàn)的select、epoll、evport和kqueue這些I/O多路復(fù)用函數(shù)庫(kù)來(lái)實(shí)現(xiàn)的,每個(gè)I/O多路復(fù)用函數(shù)庫(kù)在Redis源碼中都對(duì)應(yīng)一個(gè)單獨(dú)的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c,諸如此類。
因?yàn)镽edis為每個(gè)I/O多路復(fù)用函數(shù)庫(kù)都實(shí)現(xiàn)了相同的API,所以I/O多路復(fù)用程序的底層實(shí)現(xiàn)是可以互換的,如圖12-3所示

Redis在I/O多路復(fù)用程序的實(shí)現(xiàn)源碼中用#include宏定義了相應(yīng)的規(guī)則,程序會(huì)在編譯時(shí)自動(dòng)選擇系統(tǒng)中性能最高的I/O多路復(fù)用函數(shù)庫(kù)來(lái)作為Redis的I/O多路復(fù)用程序的底層實(shí)現(xiàn):
/* 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
# endif
12.1.3 事件的類型
I/O多路復(fù)用程序可以監(jiān)聽(tīng)多個(gè)套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和套接字操作之間的對(duì)應(yīng)關(guān)系如下:
■ 當(dāng)套接字變得可讀時(shí)(客戶端對(duì)套接字執(zhí)行write操作,或者執(zhí)行close操作),或者有新的可應(yīng)答(acceptable)套接字出現(xiàn)時(shí)(客戶端對(duì)服務(wù)器的監(jiān)聽(tīng)套接字執(zhí)行connect操作),套接字產(chǎn)生AE_READABLE事件。
■ 當(dāng)套接字變得可寫(xiě)時(shí)(客戶端對(duì)套接字執(zhí)行read操作,server維護(hù)的client wirte時(shí)),套接字產(chǎn)生AE_WRITABLE事件。
I/O多路復(fù)用程序允許服務(wù)器同時(shí)監(jiān)聽(tīng)套接字的AE_READABLE事件和AE_WRITABLE事件,如果一個(gè)套接字同時(shí)產(chǎn)生了這兩種事件,那么文件事件分派器會(huì)優(yōu)先處理AE_READABLE事件,等到AE_READABLE事件處理完之后,才處理AE_WRITABLE事件。
這也就是說(shuō),如果一個(gè)套接字又可讀又可寫(xiě)的話,那么服務(wù)器將先讀套接字,后寫(xiě)套接字。
ae.c/aeCreateFileEvent函數(shù)接受一個(gè)套接字描述符、一個(gè)事件類型,以及一個(gè)事件處理器作為參數(shù),將給定套接字的給定事件加入到I/O多路復(fù)用程序的監(jiān)聽(tīng)范圍之內(nèi),并對(duì)事件和事件處理器進(jìn)行關(guān)聯(lián)。
ae.c/aeDeleteFileEvent函數(shù)接受一個(gè)套接字描述符和一個(gè)監(jiān)聽(tīng)事件類型作為參數(shù),讓I/O多路復(fù)用程序取消對(duì)給定套接字的給定事件的監(jiān)聽(tīng),并取消事件和事件處理器之間的關(guān)聯(lián)。
ae.c/aeGetFileEvents函數(shù)接受一個(gè)套接字描述符,返回該套接字正在被監(jiān)聽(tīng)的事件類型:
■ 如果套接字沒(méi)有任何事件被監(jiān)聽(tīng),那么函數(shù)返回AE_NONE。
■ 如果套接字的讀事件正在被監(jiān)聽(tīng),那么函數(shù)返回AE_READABLE。
■ 如果套接字的寫(xiě)事件正在被監(jiān)聽(tīng),那么函數(shù)返回AE_WRITABLE。
■如果套接字的讀事件和寫(xiě)事件正在被監(jiān)聽(tīng),那么函數(shù)返回AE_READABLE | AE_WRITABLE。
ae.c/aeWait函數(shù)接受一個(gè)套接字描述符、一個(gè)事件類型和一個(gè)毫秒數(shù)為參數(shù),在給定的時(shí)間內(nèi)阻塞并等待套接字的給定類型事件產(chǎn)生,當(dāng)事件成功產(chǎn)生,或者等待超時(shí)之后,函數(shù)返回。
ae.c/aeApiPoll函數(shù)接受一個(gè)sys/time.h/struct timeval結(jié)構(gòu)為參數(shù),并在指定的時(shí)間內(nèi),阻塞并等待所有被aeCreateFileEvent函數(shù)設(shè)置為監(jiān)聽(tīng)狀態(tài)的套接字產(chǎn)生文件事件,當(dāng)有至少一個(gè)事件產(chǎn)生,或者等待超時(shí)后,函數(shù)返回
ae.c/aeProcessEvents函數(shù)是文件事件分派器,它先調(diào)用aeApiPoll函數(shù)來(lái)等待事件產(chǎn)生,然后遍歷所有已產(chǎn)生的事件,并調(diào)用相應(yīng)的事件處理器來(lái)處理這些事件。
ae.c/aeGetApiName函數(shù)返回I/O多路復(fù)用程序底層所使用的I/O多路復(fù)用函數(shù)庫(kù)的名稱:返回"epoll"表示底層為epoll函數(shù)庫(kù),返回"select"表示底層為select函數(shù)庫(kù),諸如此類。
12.1.5 文件事件的處理器
Redis為文件事件編寫(xiě)了多個(gè)處理器,這些事件處理器分別用于實(shí)現(xiàn)不同的網(wǎng)絡(luò)通信需求,比如說(shuō):
■ 為了對(duì)連接服務(wù)器的各個(gè)客戶端進(jìn)行應(yīng)答,服務(wù)器要為監(jiān)聽(tīng)套接字關(guān)聯(lián)連接應(yīng)答處理器。
■ 為了接收客戶端傳來(lái)的命令請(qǐng)求,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令請(qǐng)求處理器。
■ 為了向客戶端返回命令的執(zhí)行結(jié)果,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令回復(fù)處理器。
■ 當(dāng)主服務(wù)器和從服務(wù)器進(jìn)行復(fù)制操作時(shí),主從服務(wù)器都需要關(guān)聯(lián)特別為復(fù)制功能編寫(xiě)的復(fù)制處理器。
在這些事件處理器里面,服務(wù)器最常用的要數(shù)與客戶端進(jìn)行通信的連接應(yīng)答處理器、命令請(qǐng)求處理器和命令回復(fù)處理器。
1.連接應(yīng)答處理器
networking.c/acceptTcpHandler函數(shù)是Redis的連接應(yīng)答處理器,這個(gè)處理器用于對(duì)連接服務(wù)器監(jiān)聽(tīng)套接字的客戶端進(jìn)行應(yīng)答,具體實(shí)現(xiàn)為sys/socket.h/accept函數(shù)的包裝。
當(dāng)Redis服務(wù)器進(jìn)行初始化的時(shí)候,程序會(huì)將這個(gè)連接應(yīng)答處理器和服務(wù)器監(jiān)聽(tīng)套接字的AE_READABLE事件關(guān)聯(lián)起來(lái),當(dāng)有客戶端用sys/socket.h/connect函數(shù)連接服務(wù)器監(jiān)聽(tīng)套接字的時(shí)候,套接字就會(huì)產(chǎn)生AE_READABLE事件,引發(fā)連接應(yīng)答處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字應(yīng)答操作,如圖12-4所示。

- 命令請(qǐng)求處理器
networking.c/readQueryFrom
Client函數(shù)是Redis的命令請(qǐng)求處理器,這個(gè)處理器負(fù)責(zé)從套接字中讀入客戶端發(fā)送的命令請(qǐng)求內(nèi)容,具體實(shí)現(xiàn)為unistd.h/read函數(shù)的包裝。
當(dāng)一個(gè)客戶端通過(guò)連接應(yīng)答處理器成功連接到服務(wù)器之后,服務(wù)器會(huì)將客戶端套接字的AE_READABLE事件和命令請(qǐng)求處理器關(guān)聯(lián)起來(lái),當(dāng)客戶端向服務(wù)器發(fā)送命令請(qǐng)求的時(shí)候,套接字就會(huì)產(chǎn)生AE_READABLE事件,引發(fā)命令請(qǐng)求處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字讀入操作,如圖12-5所示。

在客戶端連接服務(wù)器的整個(gè)過(guò)程中,服務(wù)器都會(huì)一直為客戶端套接字的AE_READABLE事件關(guān)聯(lián)命令請(qǐng)求處理器。
- 命令回復(fù)處理器
networking.c/sendReplyToClient函數(shù)是Redis的命令回復(fù)處理器,這個(gè)處理器負(fù)責(zé)將服務(wù)器執(zhí)行命令后得到的命令回復(fù)通過(guò)套接字返回給客戶端,具體實(shí)現(xiàn)為unistd.h/write函數(shù)的包裝。
當(dāng)服務(wù)器有命令回復(fù)需要傳送給客戶端的時(shí)候,服務(wù)器會(huì)將客戶端套接字的AE_WRITABLE事件和命令回復(fù)處理器關(guān)聯(lián)起來(lái),當(dāng)客戶端準(zhǔn)備好接收服務(wù)器傳回的命令回復(fù)時(shí),就會(huì)產(chǎn)生AE_WRITABLE事件,引發(fā)命令回復(fù)處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字寫(xiě)入操作,如圖12-6所示。

當(dāng)命令回復(fù)發(fā)送完畢之后,服務(wù)器就會(huì)解除命令回復(fù)處理器與客戶端套接字的AE_WRITABLE事件之間的關(guān)聯(lián)。
- 一次完整的客戶端與服務(wù)器連接事件示例
讓我們來(lái)追蹤一次Redis客戶端與服務(wù)器進(jìn)行連接并發(fā)送命令的整個(gè)過(guò)程,看看在過(guò)程中會(huì)產(chǎn)生什么事件,而這些事件又是如何被處理的。
假設(shè)一個(gè)Redis服務(wù)器正在運(yùn)作,那么這個(gè)服務(wù)器的監(jiān)聽(tīng)套接字的AE_READABLE事件應(yīng)該正處于監(jiān)聽(tīng)狀態(tài)之下,而該事件所對(duì)應(yīng)的處理器為連接應(yīng)答處理器
如果這時(shí)有一個(gè)Redis客戶端向服務(wù)器發(fā)起連接,那么監(jiān)聽(tīng)套接字將產(chǎn)生AE_READABLE事件,觸發(fā)連接應(yīng)答處理器執(zhí)行。處理器會(huì)對(duì)客戶端的連接請(qǐng)求進(jìn)行應(yīng)答,然后創(chuàng)建客戶端套接字,以及客戶端狀態(tài),并將客戶端套接字的AE_READABLE事件與命令請(qǐng)求處理器進(jìn)行關(guān)聯(lián),使得客戶端可以向主服務(wù)器發(fā)送命令請(qǐng)求。
之后,假設(shè)客戶端向主服務(wù)器發(fā)送一個(gè)命令請(qǐng)求,那么客戶端套接字將產(chǎn)生AE_READABLE事件,引發(fā)命令請(qǐng)求處理器執(zhí)行,處理器讀取客戶端的命令內(nèi)容,然后傳給相關(guān)程序去執(zhí)行。
執(zhí)行命令將產(chǎn)生相應(yīng)的命令回復(fù),為了將這些命令回復(fù)傳送回客戶端,服務(wù)器會(huì)將客戶端套接字的AE_WRITABLE事件與命令回復(fù)處理器進(jìn)行關(guān)聯(lián)。當(dāng)客戶端嘗試讀取命令回復(fù)的時(shí)候,客戶端套接字將產(chǎn)生AE_WRITABLE事件(總感覺(jué)這里不對(duì),AE_WRITABLE和客戶端有關(guān)系? 明明是server在wrtie到buf后手動(dòng)設(shè)置的??!),觸發(fā)命令回復(fù)處理器執(zhí)行,當(dāng)命令回復(fù)處理器將命令回復(fù)全部寫(xiě)入到套接字之后,服務(wù)器就會(huì)解除客戶端套接字的AE_WRITABLE事件與命令回復(fù)處理器之間的關(guān)聯(lián)。
圖12-7總結(jié)了上面描述的整個(gè)通信過(guò)程,以及通信時(shí)用到的事件處理器。

12.2 時(shí)間事件
Redis的時(shí)間事件分為以下兩類:
■ 定時(shí)事件:讓一段程序在指定的時(shí)間之后執(zhí)行一次。比如說(shuō),讓程序X在當(dāng)前時(shí)間的30 毫秒之后執(zhí)行一次。
■ 周期性事件:讓一段程序每隔指定時(shí)間就執(zhí)行一次。比如說(shuō),讓程序Y 每隔30 毫秒就執(zhí)行一次。
一個(gè)時(shí)間事件主要由以下三個(gè)屬性組成:
■ id:服務(wù)器為時(shí)間事件創(chuàng)建的全局唯一ID(標(biāo)識(shí)號(hào))。ID號(hào)按從小到大的順序遞增,新事件的ID號(hào)比舊事件的ID號(hào)要大。
■ when:毫秒精度的UNIX時(shí)間戳,記錄了時(shí)間事件的到達(dá)(arrive)時(shí)間。
■ timeProc:時(shí)間事件處理器,一個(gè)函數(shù)。當(dāng)時(shí)間事件到達(dá)時(shí),服務(wù)器就會(huì)調(diào)用相應(yīng)的處理器來(lái)處理事件。
一個(gè)時(shí)間事件是定時(shí)事件還是周期性事件取決于時(shí)間事件處理器的返回值:
■ 如果事件處理器返回ae.h/AE_NOMORE,那么這個(gè)事件為定時(shí)事件:該事件在達(dá)到一次之后就會(huì)被刪除,之后不再到達(dá)。
■ 如果事件處理器返回一個(gè)非AE_NOMORE的整數(shù)值,那么這個(gè)事件為周期性時(shí)間:當(dāng)一個(gè)時(shí)間事件到達(dá)之后,服務(wù)器會(huì)根據(jù)事件處理器返回的值,對(duì)時(shí)間事件的when屬性進(jìn)行更新,讓這個(gè)事件在一段時(shí)間之后再次到達(dá),并以這種方式一直更新并運(yùn)行下去。比如說(shuō),如果一個(gè)時(shí)間事件的處理器返回整數(shù)值30,那么服務(wù)器應(yīng)該對(duì)這個(gè)時(shí)間事件進(jìn)行更新,讓這個(gè)事件在30毫秒之后再次到達(dá)。
目前版本的Redis只使用周期性事件,而沒(méi)有使用定時(shí)事件。
服務(wù)器將所有時(shí)間事件都放在一個(gè)無(wú)序鏈表中,每當(dāng)時(shí)間事件執(zhí)行器運(yùn)行時(shí),它就遍歷整個(gè)鏈表,查找所有已到達(dá)的時(shí)間事件,并調(diào)用相應(yīng)的事件處理器。
圖12-8展示了一個(gè)保存時(shí)間事件的鏈表的例子,鏈表中包含了三個(gè)不同的時(shí)間事件:因?yàn)樾碌臅r(shí)間事件總是插入到鏈表的表頭,所以三個(gè)時(shí)間事件分別按ID逆序排序,表頭事件的ID為3,中間事件的ID為2,表尾事件的ID為1。

注意,我們說(shuō)保存時(shí)間事件的鏈表為無(wú)序鏈表,指的不是鏈表不按ID排序,而是說(shuō),該鏈表不按when屬性的大小排序。正因?yàn)殒湵頉](méi)有按when屬性進(jìn)行排序,所以當(dāng)時(shí)間事件執(zhí)行器運(yùn)行的時(shí)候,它必須遍歷鏈表中的所有時(shí)間事件,這樣才能確保服務(wù)器中所有已到達(dá)的時(shí)間事件都會(huì)被處理。
在目前版本中,正常模式下的Redis服務(wù)器只使用serverCron一個(gè)時(shí)間事件,而在benchmark模式下,服務(wù)器也只使用兩個(gè)時(shí)間事件。在這種情況下,服務(wù)器幾乎是將無(wú)序鏈表退化成一個(gè)指針來(lái)使用,所以使用無(wú)序鏈表來(lái)保存時(shí)間事件,并不影響事件執(zhí)行的性能。
ae.c/aeCreateTimeEvent函數(shù)接受一個(gè)毫秒數(shù)milliseconds和一個(gè)時(shí)間事件處理器proc作為參數(shù),將一個(gè)新的時(shí)間事件添加到服務(wù)器,這個(gè)新的時(shí)間事件將在當(dāng)前時(shí)間的milliseconds毫秒之后到達(dá),而事件的處理器為proc。
例如,如果服務(wù)器當(dāng)前所保存的時(shí)間事件如圖12-9所示。

那么當(dāng)程序以50毫秒和handler_3處理器為參數(shù),在時(shí)間1385877599980(2013年12月1日零時(shí)前20毫秒)時(shí)調(diào)用aeCreateTimeEvent函數(shù),服務(wù)器將創(chuàng)建ID為3的時(shí)間事件,這時(shí)服務(wù)器所保存的時(shí)間事件將如圖12-8所示。
ae.c/aeDeleteFileEvent函數(shù)接受一個(gè)時(shí)間事件ID作為參數(shù),然后從服務(wù)器中刪除該ID所對(duì)應(yīng)的時(shí)間事件。
舉個(gè)例子,如果服務(wù)器當(dāng)前保存的時(shí)間事件如圖12-8所示,那么當(dāng)程序調(diào)用aeDeleteFileEvent(3)之后,服務(wù)器保存的時(shí)間事件將變成圖12-9所示的樣子。
ae.c/aeSearchNearestTimer函數(shù)返回到達(dá)時(shí)間距離當(dāng)前時(shí)間最接近的那個(gè)時(shí)間事件。
舉個(gè)例子,如果當(dāng)前時(shí)間為1385877599980(2013年12月1日零時(shí)前20毫秒),而服務(wù)器當(dāng)前保存的時(shí)間事件如圖12-8所示,那么調(diào)用aeSearchNearestTimer函數(shù)將返回ID為2的事件。
ae.c/processTimeEvents函數(shù)是時(shí)間事件的執(zhí)行器,這個(gè)函數(shù)會(huì)遍歷所有已到達(dá)的時(shí)間事件,并調(diào)用這些事件的處理器。已到達(dá)指的是,時(shí)間事件的when屬性記錄的UNIX時(shí)間戳等于或小于當(dāng)前時(shí)間的UNIX時(shí)間戳。
舉個(gè)例子,如果服務(wù)器保存的時(shí)間事件如圖12-8所示,并且當(dāng)前時(shí)間為1385877600010(2013年12月1日零時(shí)之后10毫秒),那么processTimeEvents函數(shù)將處理圖中ID為2和1的時(shí)間事件,因?yàn)檫@兩個(gè)事件的到達(dá)時(shí)間都大于等于1385877600010。
processTimeEvents函數(shù)的定義可以用以下偽代碼來(lái)描述
def processTimeEvents():
# 遍歷服務(wù)器中的所有時(shí)間事件
for time_event in all_time_event():
# 檢查事件是否已經(jīng)到達(dá)
if time_event.when <= unix_ts_now():
# 事件已到達(dá)
# 執(zhí)行事件處理器,并獲取返回值
retval = time_event.timeProc()
# 如果這是一個(gè)定時(shí)事件
if retval == AE_NOMORE:
# 那么將該事件從服務(wù)器中刪除
delete_time_event_from_server(time_event)
# 如果這是一個(gè)周期性事件
else:
# 那么按照事件處理器的返回值更新時(shí)間事件的 when 屬性
# 讓這個(gè)事件在指定的時(shí)間之后再次到達(dá)
update_when(time_event, retval)
12.2.3 時(shí)間事件應(yīng)用實(shí)例:serverCron函數(shù)
持續(xù)運(yùn)行的Redis服務(wù)器需要定期對(duì)自身的資源和狀態(tài)進(jìn)行檢查和調(diào)整,從而確保服務(wù)器可以長(zhǎng)期、穩(wěn)定地運(yùn)行,這些定期操作由redis.c/serverCron函數(shù)負(fù)責(zé)執(zhí)行,它的主要工作包括:
■ 更新服務(wù)器的各類統(tǒng)計(jì)信息,比如時(shí)間、內(nèi)存占用、數(shù)據(jù)庫(kù)占用情況等。
■ 清理數(shù)據(jù)庫(kù)中的過(guò)期鍵值對(duì)。
■ 關(guān)閉和清理連接失效的客戶端。
■ 嘗試進(jìn)行AOF或RDB持久化操作。
■ 如果服務(wù)器是主服務(wù)器,那么對(duì)從服務(wù)器進(jìn)行定期同步。
■ 如果處于集群模式,對(duì)集群進(jìn)行定期同步和連接測(cè)試。
Redis服務(wù)器以周期性事件的方式來(lái)運(yùn)行serverCron函數(shù),在服務(wù)器運(yùn)行期間,每隔一段時(shí)間,serverCron就會(huì)執(zhí)行一次,直到服務(wù)器關(guān)閉為止。
在Redis2.6版本,服務(wù)器默認(rèn)規(guī)定serverCron每秒運(yùn)行10次,平均每間隔100毫秒運(yùn)行一次。
從Redis2.8開(kāi)始,用戶可以通過(guò)修改hz選項(xiàng)來(lái)調(diào)整serverCron的每秒執(zhí)行次數(shù),具體信息請(qǐng)參考示例配置文件redis.conf關(guān)于hz選項(xiàng)的說(shuō)明
12.3 事件的調(diào)度與執(zhí)行
因?yàn)榉?wù)器中同時(shí)存在文件事件和時(shí)間事件兩種事件類型,所以服務(wù)器必須對(duì)這兩種事件進(jìn)行調(diào)度,決定何時(shí)應(yīng)該處理文件事件,何時(shí)又應(yīng)該處理時(shí)間事件,以及花多少時(shí)間來(lái)處理它們等等。
事件的調(diào)度和執(zhí)行由ae.c/aeProcessEvents函數(shù)負(fù)責(zé),以下是該函數(shù)的偽代碼表示:
def aeProcessEvents():
# 獲取到達(dá)時(shí)間離當(dāng)前時(shí)間最接近的時(shí)間事件
time_event = aeSearchNearestTimer()
# 計(jì)算最接近的時(shí)間事件距離到達(dá)還有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到達(dá),那么remaind_ms的值可能為負(fù)數(shù),將它設(shè)定為0
if remaind_ms < 0:
remaind_ms = 0
# 根據(jù)remaind_ms的值,創(chuàng)建timeval結(jié)構(gòu)
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件產(chǎn)生,最大阻塞時(shí)間由傳入的timeval結(jié)構(gòu)決定
# 如果remaind_ms的值為0,那么aeApiPoll調(diào)用之后馬上返回,不阻塞
aeApiPoll(timeval)
# 處理所有已產(chǎn)生的文件事件
processFileEvents()
# 處理所有已到達(dá)的時(shí)間事件
processTimeEvents()
前面在介紹文件事件API的時(shí)候,并沒(méi)有講到processFileEvents這個(gè)函數(shù),因?yàn)樗⒉淮嬖?,在?shí)際中,處理已產(chǎn)生文件事件的代碼是直接寫(xiě)在aeProcessEvents函數(shù)里面的,這里為了方便講述,才虛構(gòu)了processFileEvents函數(shù)。
將aeProcessEvents函數(shù)置于一個(gè)循環(huán)里面,加上初始化和清理函數(shù),這就構(gòu)成了Redis服務(wù)器的主函數(shù),以下是該函數(shù)的偽代碼表示:
def main():
# 初始化服務(wù)器
init_server()
# 一直處理事件,直到服務(wù)器關(guān)閉為止
while server_is_not_shutdown():
aeProcessEvents()
# 服務(wù)器關(guān)閉,執(zhí)行清理操作
clean_server()
從事件處理的角度來(lái)看,Redis服務(wù)器的運(yùn)行流程可以用流程圖12-10來(lái)概括。

以下是事件的調(diào)度和執(zhí)行規(guī)則:
1)aeApiPoll函數(shù)的最大阻塞時(shí)間由到達(dá)時(shí)間最接近當(dāng)前時(shí)間的時(shí)間事件決定,這個(gè)方法既可以避免服務(wù)器對(duì)時(shí)間事件進(jìn)行頻繁的輪詢(忙等待),也可以確保aeApiPoll函數(shù)不會(huì)阻塞過(guò)長(zhǎng)時(shí)間。
2)因?yàn)槲募录请S機(jī)出現(xiàn)的,如果等待并處理完一次文件事件之后,仍未有任何時(shí)間事件到達(dá),那么服務(wù)器將再次等待并處理文件事件。隨著文件事件的不斷執(zhí)行,時(shí)間會(huì)逐漸向時(shí)間事件所設(shè)置的到達(dá)時(shí)間逼近,并最終來(lái)到到達(dá)時(shí)間,這時(shí)服務(wù)器就可以開(kāi)始處理到達(dá)的時(shí)間事件了。
3)對(duì)文件事件和時(shí)間事件的處理都是同步、有序、原子地執(zhí)行的,服務(wù)器不會(huì)中途中斷事件處理,也不會(huì)對(duì)事件進(jìn)行搶占,因此,不管是文件事件的處理器,還是時(shí)間事件的處理器,它們都會(huì)盡可地減少程序的阻塞時(shí)間,并在有需要時(shí)主動(dòng)讓出執(zhí)行權(quán),從而降低造成事件饑餓的可能性。比如說(shuō),在命令回復(fù)處理器將一個(gè)命令回復(fù)寫(xiě)入到客戶端套接字時(shí),如果寫(xiě)入字節(jié)數(shù)超過(guò)了一個(gè)預(yù)設(shè)常量的話,命令回復(fù)處理器就會(huì)主動(dòng)用break跳出寫(xiě)入循環(huán),將余下的數(shù)據(jù)留到下次再寫(xiě);另外,時(shí)間事件也會(huì)將非常耗時(shí)的持久化操作放到子線程或者子進(jìn)程執(zhí)行。
4)因?yàn)闀r(shí)間事件在文件事件之后執(zhí)行,并且事件之間不會(huì)出現(xiàn)搶占,所以時(shí)間事件的實(shí)際處理時(shí)間,通常會(huì)比時(shí)間事件設(shè)定的到達(dá)時(shí)間稍晚一些。
表12-1 記錄了一次完整的事件調(diào)度和執(zhí)行過(guò)程。

表12-1記錄的事件執(zhí)行過(guò)程凸顯了上面列舉的事件調(diào)度規(guī)則中的規(guī)則2、3、4:
■ 因?yàn)闀r(shí)間事件尚未到達(dá),所以在處理時(shí)間事件之前,服務(wù)器已經(jīng)等待并處理了兩次文件事件。
■ 因?yàn)樘幚硎录倪^(guò)程中不會(huì)出現(xiàn)搶占,所以實(shí)際處理時(shí)間事件的時(shí)間比預(yù)定的100 毫秒慢了30 毫秒。
總結(jié)
■ Redis服務(wù)器是一個(gè)事件驅(qū)動(dòng)程序,服務(wù)器處理的事件分為時(shí)間事件和文件事件兩類。
■ 文件事件處理器是基于Reactor模式實(shí)現(xiàn)的網(wǎng)絡(luò)通信程序。
■ 文件事件是對(duì)套接字操作的抽象:每次套接字變?yōu)榭蓱?yīng)答(acceptable)、可寫(xiě)(writable)或者可讀(readable)時(shí),相應(yīng)的文件事件就會(huì)產(chǎn)生。
■ 文件事件分為AE_READABLE事件(讀事件)和AE_WRITABLE事件(寫(xiě)事件)兩類。
■ 時(shí)間事件分為定時(shí)事件和周期性事件:定時(shí)事件只在指定的時(shí)間到達(dá)一次,而周期性事件則每隔一段時(shí)間到達(dá)一次。
■ 服務(wù)器在一般情況下只執(zhí)行serverCron函數(shù)一個(gè)時(shí)間事件,并且這個(gè)事件是周期性事件。
■ 文件事件和時(shí)間事件之間是合作關(guān)系,服務(wù)器會(huì)輪流處理這兩種事件,并且處理事件的過(guò)程中也不會(huì)進(jìn)行搶占。
■ 時(shí)間事件的實(shí)際處理時(shí)間通常會(huì)比設(shè)定的到達(dá)時(shí)間晚一些。