Redis 文件事件

2018-08-02 14:52 更新

Redis 基于 Reactor 模式開(kāi)發(fā)了自己的網(wǎng)絡(luò)事件處理器: 這個(gè)處理器被稱為文件事件處理器(file 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)、寫入(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)單性。

文件事件處理器的構(gòu)成

圖 IMAGE_CONSTRUCT_OF_FILE_EVENT_HANDLER 展示了文件事件處理器的四個(gè)組成部分, 它們分別是套接字、 I/O 多路復(fù)用程序、 文件事件分派器(dispatcher)、 以及事件處理器。

文件事件是對(duì)套接字操作的抽象, 每當(dāng)一個(gè)套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、寫入、讀取、關(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/O 多路復(fù)用程序總是會(huì)將所有產(chǎn)生事件的套接字都入隊(duì)到一個(gè)隊(duì)列里面, 然后通過(guò)這個(gè)隊(duì)列, 以有序(sequentially)、同步(synchronously)、每次一個(gè)套接字的方式向文件事件分派器傳送套接字: 當(dāng)上一個(gè)套接字產(chǎn)生的事件被處理完畢之后(該套接字為事件所關(guān)聯(lián)的事件處理器執(zhí)行完畢), I/O 多路復(fù)用程序才會(huì)繼續(xù)向文件事件分派器傳送下一個(gè)套接字, 如圖 IMAGE_DISPATCH_EVENT_VIA_QUEUE 。

文件事件分派器接收 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)作。

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)?Redis 為每個(gè) I/O 多路復(fù)用函數(shù)庫(kù)都實(shí)現(xiàn)了相同的 API , 所以 I/O 多路復(fù)用程序的底層實(shí)現(xiàn)是可以互換的, 如圖 IMAGE_MULTI_LIB 所示。

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

事件的類型

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)套接字變得可寫時(shí)(客戶端對(duì)套接字執(zhí)行 read 操作), 套接字產(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è)套接字又可讀又可寫的話, 那么服務(wù)器將先讀套接字, 后寫套接字。

API

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 。
  • 如果套接字的寫事件正在被監(jiān)聽(tīng), 那么函數(shù)返回 AE_WRITABLE 。
  • 如果套接字的讀事件和寫事件正在被監(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ù), 諸如此類。

文件事件的處理器

Redis 為文件事件編寫了多個(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ù)制功能編寫的復(fù)制處理器。
  • 等等。

在這些事件處理器里面, 服務(wù)器最常用的要數(shù)與客戶端進(jìn)行通信的連接應(yīng)答處理器、 命令請(qǐng)求處理器和命令回復(fù)處理器。

連接應(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)答操作, 如圖 IMAGE_SERVER_ACCEPT_CONNECT 所示。

命令請(qǐng)求處理器

networking.c/readQueryFromClient 函數(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)的套接字讀入操作, 如圖 IMAGE_SERVER_RECIVE_COMMAND_REQUEST 所示。

在客戶端連接服務(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)的套接字寫入操作, 如圖 IMAGE_SERVER_SEND_REPLY 所示。

當(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 事件, 觸發(fā)命令回復(fù)處理器執(zhí)行, 當(dāng)命令回復(fù)處理器將命令回復(fù)全部寫入到套接字之后, 服務(wù)器就會(huì)解除客戶端套接字的 AE_WRITABLE 事件與命令回復(fù)處理器之間的關(guān)聯(lián)。

圖 IMAGE_COMMAND_PROGRESS 總結(jié)了上面描述的整個(gè)通訊過(guò)程, 以及通訊時(shí)用到的事件處理器。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)