Redis 採用事件驅動機制來處理大量的網絡IO。它並沒有使用 libevent 或者 libev 這樣的成熟開源方案,而是自己實現一個非常簡潔的事件驅動庫 ae_event。
Redis中的事件驅動庫只關注網絡IO,以及定時器。該事件庫處理下面兩類事件:
事件驅動庫的代碼主要是在src/ae.c中實現的,其示意圖如下所示。
aeEventLoop 是整個事件驅動的核心,它管理著文件事件表和時間事件列表,不斷地循環處理著就緒的文件事件和到期的時間事件。下面我們就先分別介紹文件事件和時間事件,然後講述相關的 aeEventLoop 源碼實現。
文件事件
Redis基於Reactor模式開發了自己的網絡事件處理器,也就是文件事件處理器。文件事件處理器使用IO多路復用技術,同時監聽多個套接字,並為套接字關聯不同的事件處理函數。當套接字的可讀或者可寫事件觸發時,就會調用相應的事件處理函數。
Redis 使用的IO多路復用技術主要有: select 、 epoll 、 evport 和 kqueue等。每個IO多路復用函數庫在 Redis 源碼中都對應一個單獨的文件,比如ae select.c,ae epoll.c, ae_kqueue.c等。Redis 會根據不同的作業系統,按照不同的優先級選擇多路復用技術。事件響應框架一般都採用該架構,比如 netty 和 libevent。
如下圖所示,文件事件處理器有四個組成部分,它們分別是套接字、I/O多路復用程序、文件事件分派器以及事件處理器。
文件事件是對套接字操作的抽象,每當一個套接字準備好執行 accept、read、write和 close 等操作時,就會產生一個文件事件。因為 Redis 通常會連接多個套接字,所以多個文件事件有可能並發的出現。
I/O多路復用程序負責監聽多個套接字,並向文件事件派發器傳遞那些產生了事件的套接字。
儘管多個文件事件可能會並發地出現,但I/O多路復用程序總是會將所有產生的套接字都放到同一個隊列(也就是後文中描述的 aeEventLoop 的 fired 就緒事件表)裡邊,然後文件事件處理器會以有序、同步、單個套接字的方式處理該隊列中的套接字,也就是處理就緒的文件事件。
所以,一次 Redis 客戶端與伺服器進行連接並且發送命令的過程如上圖所示。
時間事件
Redis 的時間事件分為以下兩類:
Redis 的時間事件的具體定義結構如下所示。
一個時間事件是定時事件還是周期性事件取決於時間處理器的返回值:
Redis 將所有時間事件都放在一個無序鍊表中,每次 Redis 會遍歷整個鍊表,查找所有已經到達的時間事件,並且調用相應的事件處理器。
介紹完文件事件和時間事件,我們接下來看一下 aeEventLoop 的具體實現。
創建事件管理器
Redis 服務端在其初始化函數 initServer 中,會創建事件管理器 aeEventLoop 對象。
函數 aeCreateEventLoop 將創建一個事件管理器,主要是初始化 aeEventLoop 的各個屬性值,比如 events 、 fired 、 timeEventHead 和 apidata :
aeApiCreate 函數首先創建了 aeApiState 對象,初始化了epoll就緒事件表;然後調用 epoll_create 創建了 epoll 實例,最後將該 aeApiState 賦值給 apidata 屬性。
aeApiState 對象中 epfd 存儲 epoll 的標識, events 是一個 epoll 就緒事件數組,當有 epoll 事件發生時,所有發生的 epoll 事件和其描述符將存儲在這個數組中。這個就緒事件數組由應用層開闢空間、內核負責把所有發生的事件填充到該數組。
創建文件事件
aeFileEvent 是文件事件結構,對於每一個具體的事件,都有讀處理函數和寫處理函數等。Redis 調用 aeCreateFileEvent 函數針對不同的套接字的讀寫事件註冊對應的文件事件。
比如說,Redis 進行主從複製時,從伺服器需要主伺服器建立連接,它會發起一個 socekt連接,然後調用 aeCreateFileEvent 函數針對發起的socket的讀寫事件註冊了對應的事件處理器,也就是 syncWithMaster 函數。
aeCreateFileEvent 的參數 fd 指的是具體的 socket 套接字, proc 指 fd 產生事件時,具體的處理函數, clientData 則是回調處理函數時需要傳入的數據。 aeCreateFileEvent 主要做了三件事情:
如上文所說,Redis 基於的底層 I/O 多路復用庫有多套,所以 aeApiAddEvent 也有多套實現,下面的源碼是 epoll 下的實現。其核心操作就是調用 epoll 的 epoll_ctl 函數來向 epoll 註冊響應事件。有關 epoll 相關的知識可以看一下《Java NIO源碼分析》
事件處理
因為 Redis 中同時存在文件事件和時間事件兩個事件類型,所以伺服器必須對這兩個事件進行調度,決定何時處理文件事件,何時處理時間事件,以及如何調度它們。
aeMain 函數以一個無限循環不斷地調用 aeProcessEvents 函數來處理所有的事件。
下面是 aeProcessEvents 的偽代碼,它會首先計算距離當前時間最近的時間事件,以此計算一個超時時間;然後調用 aeApiPoll 函數去等待底層的I/O多路復用事件就緒; aeApiPoll函數返回之後,會處理所有已經產生文件事件和已經達到的時間事件。
與 aeApiAddEvent 類似, aeApiPoll 也有多套實現,它其實就做了兩件事情,調用 epoll_wait 阻塞等待 epoll 的事件就緒,超時時間就是之前根據最快達到時間事件計算而來的超時時間;然後將就緒的 epoll 事件轉換到fired就緒事件。 aeApiPoll 就是上文所說的I/O多路復用程序。具體過程如下圖所示。
processFileEvent 是處理就緒文件事件的偽代碼,也是上文所述的文件事件分派器,它其實就是遍歷 fired 就緒事件表,然後根據對應的事件類型來調用事件中註冊的不同處理器,讀事件調用 rfileProc ,而寫事件調用 wfileProc 。
而 processTimeEvents 是處理時間事件的函數,它會遍歷 aeEventLoop 的事件事件列表,如果時間事件到達就執行其 timeProc 函數,並根據函數的返回值是否等於 AE_NOMORE來決定該時間事件是否是周期性事件,並修改器到達時間。
刪除事件
當不在需要某個事件時,需要把事件刪除掉。例如: 如果fd同時監聽讀事件、寫事件。當不在需要監聽寫事件時,可以把該fd的寫事件刪除。
aeDeleteEventLoop 函數的執行過程總結為以下幾個步驟 1、根據 fd 在未就緒表中查找到事件 2、取消該 fd 對應的相應事件標識符 3、調用 aeApiFree 函數,內核會將epoll監聽紅黑樹上的相應事件監聽取消。