《Redis設計與實現》[第二部分]單機數據庫的實現-C源碼閱讀(四)

4、事件

關鍵字:I/O併發模式,文件事件處理器,時間事件處理器

Redis服務器是一個事件驅動程序,服務器需要處理兩類事件:

  • 文件事件(file event):Redis服務器通過套接字與客戶端(或其他Redis服務器)進行連接,而文件事件就是服務器對套接字操作的抽象。服務器與客戶端(或其他服務器)的通信會產生相應的文件事件,而服務器則通過監聽並處理這些事件來完成一系列網絡通信操作
  • 時間事件(Time event):Redis服務器中的一些操作(如serverCron函數)需要在給定的時間點執行,而時間事件就是服務器對這類定時操作的抽象

文件事件

Reactor模式是事件驅動的,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers,這個Service Handler會同步地將輸入的請求(Event)多路複用的分發給相應的Request Handler。

Redis基於Reactor模式開發了自己的網絡事件處理器:這個處理器被稱爲文件事件處理器(file event handler)。

  • 文件事件處理器使用I/O多路複用(multiplexing)程序來同時監聽多個套接字,並根據套接字目前執行的任務來爲套接字關聯不同的事件處理器
  • 當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

雖然文件事件處理器以單線程方式運行,但通過使用I/O多路複用程序來監聽多個套接字,文件事件處理器既實現了高性能的網絡通信模型,又可以很好地與redis服務器中其他同樣以單線程方式運行的模塊進行對接,這使得redis保持了內部單線程設計的簡單性。

fileevent.png

文件事件處理器有四個組成部分:

  • 套接字:

    • 文件事件是對套接字操作的抽象,每當一個套接字準備好執行連接應答(accept)、寫入、讀取、關閉等操作時,就會產生一個文件事件。
    • 因爲一個服務器通常會連接多個套接字,所以多個文件事件有可能會併發地出現。
  • I/O多路複用程序:

    • I/O多路複用程序負責監聽多個套接字,並向文件事件分派器傳送那些產生了事件的套接字。

    • 儘管多個文件事件可能會併發地出現,但I/O多路複用程序總是會將所有產生事件的套接字都放到一個隊列裏,然後通過這個隊列,以有序(sequentially)、同步(synchronously)、每次一個套接字 的方式向文件事件分派器傳送套接字。

    • 當上一個套接字產生的事件被處理完畢之後(與該套接字事件所關聯的事件處理器執行完畢),I/O多路複用程序纔會繼續向文件事件分派器傳送下一個套接字

  • 文件事件分派器(dispatcher):

    • 接受I/O多路複用程序傳來的套接字,並根據套接字產生的事件的類型,調用相應的事件處理器
  • 事件處理器:

    • 服務器會爲執行不同任務的套接字關聯不同的事件處理器,這些處理器是一個個函數,它們定義了某個事件發生時,服務器應該執行的動作

Redis的I/O多路複用程序的所有功能都是通過包裝常見的select、epoll、evport和kqueue這些I/O多路複用函數庫來實現的,體現在Redis源碼中的ae_select.c、ae_epoll.c、ae_kqueue.c等

因爲redis爲每個I/O多路複用函數庫都實現了相同的API,所以I/O多路複用程序的底層實現是可以互換的。

Redis在I/O多路複用程序的實現源碼中用#include宏定義了相應的規則,程序會在編譯時自動選擇系統中性能最高的I/O多路複用函數庫作爲Redis的I/O多路複用程序底層實現。

/* 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多路複用程序可以監聽多個套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和套接字操作之間的對應關係如下:
- 當套接字變得可讀時,(客戶端對套接字執行write操作,或者執行close操作),或者有新的可應答(acceptable)套接字出現時,(客戶端對服務器的監聽套接字執行connect操作),套接字產生AE_READABLE事件。
- 當套接字變得可寫時,(客戶端對套接字執行read操作),套接字產生AE_WRITABLE事件。

I/O多路複用程序允許服務器同時監聽套接字的AE_READABLE事件和AE_WRITABLE事件,如果一個套接字同時產生了這兩種事件,那麼文件事件分派器會優先處理AE_READABLE事件,等到AE_READABLE事件處理完之後,才處理AE_WRITABLE事件。
即,如果一個套接字既可讀又可寫,那麼服務器將先讀套接字,後寫套接字。
- ae.c/aeCreateFileEvent函數接受一個套接字描述符、一個事件類型,以及一個事件處理器作爲參數,將給定套接字的給定事件加入到I/O多路複用程序的監聽範圍之內,並對事件和事件處理器進行關聯。
- ae.c/aeDeleteFileEvent函數接受一個套接字描述符和一個監聽事件類型作爲參數,讓I/O多路複用程序取消對給定套接字的給定事件的監聽,並取消事件和事件處理器之間的關聯
- ae.c/aeGetFileEvents函數接受一個套接字描述符,返回該套接字正在被監聽的事件類型:
+ 沒有任何事件被監聽,函數返回AE_NONE
+ 讀事件正在被監聽,函數返回AE_READABLE
+ 寫事件正在被監聽,函數返回AE_WRITABLE
+ 讀和寫事件正在被監聽,函數返回AE_READABLE | AE_WRITABLE

文件事件處理器

文件事件處理器:
- 服務器爲監聽套接字關聯連接應答處理器,對連接服務器的各個客戶端進行應答
- 服務器爲客戶端套接字關聯命令請求處理器,接收客戶端傳來的命令請求
- 服務器爲客戶端套接字關聯命令回覆處理器,向客戶端返回命令的執行結果
- 主從服務器都需要關聯特別爲複製功能編寫的複製處理器,用於主服務器和從服務器的複製操作

連接應答處理器

networking.c/acceptTcpHandler函數是Redis的連接應答處理器,用於對連接服務器監聽套接字的客戶端進行應答,具體實現爲sys/socket.h/accept函數的包裝。

客戶端使用sys/socket.h/connect函數連接服務器監聽套接字的時候,套接字就會產生AE_READABLE事件,引發連接應答處理器執行。

命令請求處理器

networking.c/readQueryFromClient,具體實現爲unistd.h/read函數的包裝

命令回覆處理器

networking.c/sendReplyToClient,具體實現爲unistd.h/write函數的包裝

時間事件

Redis的時間事件
- 定時事件:指定時間執行一次
- 週期性事件:每隔指定時間執行一次

時間事件主要由三個屬性組成:
- id:全局唯一id
- when:毫秒精度的UNIX時間戳,記錄時間事件的到達時間
- timeProc:時間事件處理器,一個函數。

一個時間事件是定時事件還是週期性事件取決於時間事件處理器的返回值:
- 如果事件處理器返回ae.h/AE_NOMORE,定時事件
- 非AE_NOMORE,週期性事件,當一個時間事件到達後,服務器會根據事件處理器返回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間後再次到達

API

  • ae.c/aeCreateTimeEvent
  • ae.c/aeDeleteFileEvent
  • ae.c/aeSearchNearestTimer:返回到達時間距離當前時間最接近的那個時間事件
  • ae.c/processTimeEvents:遍歷所有已到達的時間事件,調用其處理器
def processTimeEvents:
    for time_event in all_time_event():
        if time_event.when <= unix_ts_now():
            retval = time_event.timeProc()
            if retval == AE_NOMORE:
                delete_time_event_from_server(time_event)
        else:
            update_when(time_event, retval)

事件的調度與執行

def aeProcessEvents():
    time_event = aeSearchNearestTimer()
    remaind_ms = time_event.when - unix_ts_now()
    if remaind_ms < 0:
        remaind_ms = 0
    timeval = create_timeval_with_ms(remaind_ms)
    aeApiPoll(timeval)
    processFileEvents()
    processTimeEvents()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章