標籤:程式 date 大於等於 再處理 複製 全域 次數 中間 nts
事件
Redis伺服器是一個事件驅動程式,伺服器需要處理以下兩類事情:
- 檔案事件(file event):Redis伺服器通過通訊端與用戶端(或者其他Redis伺服器)進行串連,而檔案事件就是伺服器對通訊端操作的抽象。伺服器與用戶端(或者其他伺服器)的通訊會產生相應的檔案事件,而伺服器則通過監聽並處理這些事件來完成一系列網路通訊操作
- 時間事件(time event):Redis伺服器中的一些操作(比如serverCron函數)需要在給定的時間點執行,而時間事件就是伺服器對這類定時操作的抽象
檔案事件
Redis是基於Reactor模式開發了自己的網路事件處理器,這個處理器被稱為檔案事件處理器:
- 檔案事件處理器使用I/O多工程式來同時監聽多個通訊端,並根據通訊端目前執行的任務來為通訊端關聯不同的事件處理器
- 當被監聽的通訊端準備好執行串連應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的檔案事件就會產生,這時檔案事件處理器就會調用通訊端之前關聯好的事件處理器來處理這些事件
雖然檔案事件處理器以單線程方式運行,但通過使用I/O多工程式來監聽多個通訊端,檔案事件處理器既實現了高效能的網路通訊模型,又可以很好地與Redis伺服器中其他同樣以單線程方式啟動並執行模組進行對接,這保持了Redis內部單線程設計的簡單性
檔案事件處理器的構成
圖1-1展示了檔案事件處理器的四個組成部分,它們分別是通訊端、I/O多工程式、檔案事件指派器,以及事件處理器
圖1-1 檔案事件處理器的四個組成部分
檔案事件是對通訊端操作的抽象,每當一個通訊端準備好執行串連應答(accept)、寫入、讀取、關閉等操作時,就會產生一個檔案事件。因為一個伺服器通常會串連多個通訊端,所以多個檔案事件有可能會並發地出現。I/O多工程式負責監聽多個通訊端,並向檔案事件指派器傳送那些產生了事件的通訊端
儘管多個檔案事件可能會並發地出現,但I/O多工程式總是會將所有產生事件的通訊端都放到一個隊列中,然後通過這個隊列,以有序、同步、每次一個通訊端的方式向檔案事件指派器傳送通訊端。當上一個通訊端產生的實踐被處理完畢之後(該通訊端為事件所關聯的事件處理器執行完畢),I/O多工程式才會繼續向檔案事件指派器傳送下一個通訊端,1-2所示
圖1-2 I/O多工程式通過隊列向檔案事件指派器傳送通訊端
檔案事件指派器接收I/O多工程式傳來的通訊端,並根據通訊端產生的事件的類型,調用相應的事件處理器。伺服器會為執行不同任務的通訊端關聯不同的事件處理器,這些處理器是一個個函數,它們定義了某個事件發生時,伺服器應該執行的動作
I/O多工程式的實現
Redis的I/O多工程式的所有功能都是通過封裝常見的select、epoll、evport和kqueue這些I/O多工函數庫來實現的,每個I/O多工函數庫在Redis源碼中都對應一個單獨的檔案,比如ae_select.c、ae_epoll.c、ae_kqueue.c,諸如此類。因為Redis為每個I/O多工函數庫都實現了相同的API,所以I/O多工程式的底層實現時可以互換的,1-3所示
圖1-3 Redis的I/O多工程式有多個I/O多工庫實現可選
Redis在I/O多工程式的實現源碼中用#include宏定義了相應的規則,程式會在編譯時間自動選擇系統中效能最高的I/O多工函數庫來作為Redis的I/O多工程式的底層實現
ae.c
/* 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
事件的類型
/O多工程式可以監聽多個通訊端的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和通訊端操作之間的對應關係如下:
- 當通訊端變得可讀時(用戶端對通訊端執行write操作,或執行close操作),或有新的可應答(acceptable)通訊端出現時(用戶端對伺服器的監聽通訊端執行connect操作),通訊端產生AE_READABLE事件
- 當通訊端變得可寫時(用戶端對通訊端執行read操作),通訊端產生AE_WRITABLE事件
/O多工程式允許伺服器同時監聽通訊端的AE_READABLE事件和AE_WRITABLE事件,如果一個通訊端同時產生了這兩種事件,那麼檔案指派器會優先處理AE_READABLE事件之後,再處理AE_WRITABLE事件。也就是說,如果一個通訊端可讀又可寫的話,那麼伺服器會先讀取通訊端,後寫通訊端
API
- ae.c/aeCreateFileEvent函數接受一個通訊端描述符、一個事件類型、以及一個事件處理器作為參數,將給定通訊端的事件加入到I/O多工程式的監聽範圍內,並對事件和事件處理器進行關聯
- ae.c/aeDeleteFileEvent函數接受一個通訊端描述符和一個監聽事件類型作為參數,讓I/O多工程式取消對給定通訊端的給定事件的監聽,並取消事件和事件處理器之間的關聯
- ae.c/aeGetFileEvents函數接受一個通訊端描述符,返回該通訊端正在被監聽的事件類型:
- 如果通訊端沒有任何事件被監聽,那麼函數返回AE_NONE
- 如果通訊端的讀事件正在被監聽,那麼函數返回AE_READABLE
- 如果通訊端的寫事件正在被監聽,那麼函數返回AE_WRITABLE
- 如果通訊端的讀事件和寫事件正在被監聽,那麼函數返回AE_READABLE|AE_WRITABLE
- ae.c/aeWait函數接受一個通訊端描述符、一個事件類型和一個毫秒數為參數,在給定的時間內阻塞並等待通訊端的給定類型事件產生,當事件成功產生,或者等待逾時之後,函數返回
- ae.c/aeApiPoll函數接受一個sys/time.h/struct timeval結構為參數,並在指定的時間內,阻塞並等待所有被aeCreateFileEvent函數設定為監聽狀態的通訊端產生檔案事件,當有至少一個事件產生,或者等待逾時後,函數返回
- ae.c/aeProcessEvents函數是檔案事件指派器,它先調用aeApiPoll函數來等待事件產生,然後遍曆所有已產生的事件,並調用相應的事件處理器來處理這些事件
- ae.c/aeGetApiName函數返回I/O多工程式底層所使用的I/O多工函數庫的名稱:返回"select"表示底層為select函數庫,諸如此類
檔案事件的處理器
Redis為檔案事件編寫了多個處理器,這些事件處理器分別用於實現不同的網路通訊需求,比如說:
- 為了對串連伺服器的各個用戶端進行應答,伺服器要為監聽通訊端關聯串連應答處理器
- 為了接收用戶端傳來的命令請求,伺服器要為用戶端通訊端關聯命令要求處理常式
- 為了向用戶端返回命令的執行結果,伺服器要為用戶端通訊端關聯命令回複處理器
- 當主伺服器和從伺服器進行複製操作時,主從伺服器都需要關聯特別為複製功能編寫的複製處理器
在這些事件處理器中,伺服器最常用的要數與用戶端進行通訊的串連應答處理器、命令要求處理常式和命令回複處理器
串連應答處理器
networking.c/acceptTcpHandler函數是Redis的串連應答處理器,這個處理器用於對串連伺服器監聽通訊端的用戶端進行應答,具體實現為為sys/socket.h/accept函數的封裝。當Redis伺服器進行初始化的時候,程式會將這個串連應答處理器和服務監聽通訊端的AE_READABLE事件關聯起來,當有用戶端用sys/socket.h/connect函數串連服務端監聽通訊端時,通訊端就會產生AE_READABLE事件,引發串連應答處理器執行,並執行相應的通訊端應答操作,1-4所示
圖1-4 伺服器對用戶端的串連請求進行應答
命令要求處理常式
networking.c/readQueryFromClient函數是Redis的命令要求處理常式,這個處理器負責從通訊端中讀入用戶端發送的命令請求內容,具體實現為unistd.h/read函數的封裝。當一個用戶端通過串連應答處理器成功串連到伺服器之後,伺服器會將用戶端通訊端的AE_READABLE事件和命令要求處理常式關聯起來,當用戶端向伺服器發送命令請求的時候,通訊端就會產生AE_READABLE事件,引發命令要求處理常式執行,並執行相應的通訊端讀入操作,1-5所示。在用戶端串連伺服器的整個過程中,伺服器都會一直為用戶端通訊端的AE_READABLE事件關聯命令要求處理常式
圖1-5 伺服器接收用戶端發送來的命令請求
命令回複處理器
networking.c/sendReplyToClient函數是Redis的命令回複處理器,這個處理器負責將伺服器執行命令後得到的命令回複通過通訊端返回給用戶端,具體實現為unistd.h/write函數的封裝。當伺服器有命令回複需要傳送給用戶端的時候,伺服器會將用戶端通訊端的AE_WRITABLE事件和命令回複處理器關聯起來,當用戶端準備好接收伺服器傳回的命令回複時,就會產生AE_WRITABLE事件,引發命令回複處理器執行,並執行相應的通訊端寫入操作,1-6所示。當命令回複發送完畢之後,伺服器就會解除命令回複處理器與用戶端通訊端的AE_WRITABLE事件之間的關聯
圖1-6 伺服器向用戶端發送命令回複
一次完整的用戶端與伺服器串連事件樣本
假設一個Redis伺服器正在運作,那麼這個伺服器的監聽通訊端的AE_READABLE事件應該正處於監聽狀態下,而該事件所對應的處理器為串連應答處理器。如果這時有一個Redis用戶端向伺服器發起串連,那麼監聽通訊端將產生AE_READABLE事件,觸發串連應答處理器執行。處理器會對用戶端的串連請求進行應答,然後建立用戶端通訊端,以及用戶端狀態,並向用戶端通訊端的AE_READABLE事件與命令要求處理常式進行關聯,使得用戶端可以向主伺服器發送命令請求
之後,假設用戶端向主伺服器發送一個命令請求,那麼用戶端通訊端將產生AE_READABLE事件,引發命令要求處理常式執行,處理器讀取用戶端命令內容,然後傳給相關程式去執行。執行命令將產生相應的命令回複,為了將這些命令回複傳送給用戶端,伺服器會將用戶端通訊端的AE_WRITABLE事件與命令回複處理器進行關聯。當用戶端嘗試讀取命令回複時,用戶端通訊端將產生AE_WRITABLE事件,觸發命令回複處理器執行,當命令回複處理器將命令回複全部寫入到通訊端之後,伺服器就會解除用戶端通訊端的AE_WRITABLE事件與命令回複處理器之間的關聯
圖1-7總結了上面描述的整個通訊過程,以及通訊時用到的事件處理器
圖1-7 用戶端和伺服器的通訊過程
時間事件
Redis的時間事件分為以下兩類:
- 定時事件:讓一段程式在指定的時間之後執行一次。比如說,讓程式X在目前時間的30毫秒之後執行一次
- 周期性事件:讓一段程式每隔指定時間就執行一次。比如說,讓程式Y每隔30毫秒就執行一次
一個時間事件主要由以下三個屬性群組成:
- id:伺服器為時間事件建立的全域唯一ID(標識號)。ID號按從小到大的順序遞增,新事件的ID號比舊事件的ID號要大
- when:毫秒精度的UNIX時間戳記,記錄了時間事件的到達(arrive)時間
- timeProc:時間事件處理器,一個函數。當時間事件到達時,伺服器就會調用相應的處理器來處理事件
一個時間事件是定時事件還是周期性事件取決於時間事件處理器的傳回值:
- 如果事件處理器返回ae.h/AE_NOMORE,那麼這個事件為定時事件:該事件在達到一次之後就會被刪除,之後不再到達
- 如果事件處理器返回一個非AE_NOMORE的整數值,那麼這個事件為周期性時間:當一個時間事件到達之後,伺服器會根據事件處理器返回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間之後再次到達,並以這種方式一直更新並運行下去。比如說,如果一個時間事件的處理器返回整數值30,那麼伺服器應該對這個時間事件進行更新,讓這個事件在30毫秒之後再次到達
實現
伺服器將所有時間事件都放在一個無序鏈表中,每當時間事件執行器運行時,它就遍曆整個鏈表,尋找所有已達的時間事件,並調用相應的事件處理器。圖1-8展示了一個儲存時間事件的鏈表的例子,鏈表中包含了三個不同的時間事件:因為新的時間事件總是插入到鏈表的表頭,所以三個時間事件分別按ID逆序排序,表頭事件的ID為3,中間事件的ID為2,表尾事件的ID為1
圖1-8 用鏈表串連起來的三個時間事件
我們說儲存時間事件的鏈表為無序鏈表,指的不是鏈表不按ID排序,而是說,該鏈表不按when屬性的大小排序。正因為鏈表沒有按when屬性進行排序,所以當時間事件執行器啟動並執行時候,它必須遍曆鏈表中的所有時間事件,這樣才能確保伺服器中所有已達到時間事件都會被處理
API
- ae.c/aeCreateTimeEvent函數接受一個毫秒數milliseconds和一個時間事件處理器proc作為參數,將一個新的時間事件添加到伺服器,這個新的時間事件將在目前時間的milliseconds毫秒之後到達,而事件的處理器為proc。
例如,如果伺服器當前儲存的時間事件1-9所示,那麼當程式以50毫秒和handler_3處理器為參數,在時間1385877599980(2013年12月1日零時前20毫秒)時調用aeCreateTimeEvent函數,伺服器將建立ID為3的時間事件,這時伺服器所儲存的時間事件將1-8所示
圖1-9 用鏈表串連起來的兩個時間事件
- ae.c/aeDeleteFileEvent函數接受一個時間事件ID作為參數,然後從伺服器中刪除該ID所對應的時間事件
- ae.c/aeSearchNearestTimer函數返回到達時間距離目前時間最接近的那個時間事件
- ae.c/processTimeEvents函數是時間事件的執行器,這個函數會遍曆所有已到達的時間事件,並調用這些事件的處理器。已到達指的是,時間事件的when屬性記錄的Unix時間戳記等於或小於目前時間的Unix時間戳記
舉個栗子,如果伺服器儲存的時間事件1-8所示,並且目前時間為1385877600010(2013年12月1日零時之後10毫秒),那麼processTimeEvents函數將處理圖中ID為2和1的時間事件,因為這兩個時間的到達時間都大於等於1385877600010
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: #那麼按照事件處理器的傳回值更新時間事件的when屬性 #讓這個事件在指定的時間之後再次到達 update_when(time_event, retval)
時間事件應用執行個體:serverCron函數
持續啟動並執行Redis伺服器需要定期對自身的資源和狀態進行檢查和調整,從而確保伺服器可以長期、穩定地運行,這些定期操作由redis.c/serverCron函數負責執行,它的主要工作包括:
- 補救伺服器的各類統計資訊,比如時間、記憶體佔用、資料庫佔用情況等
- 清理資料庫中的到期索引值對
- 關閉和清理串連失效的用戶端
- 嘗試進行AOF或RDB持久化操作
- 如果伺服器是主伺服器,那麼對從伺服器進行定期同步
- 如果處於叢集模式,對叢集進行定期同步和串連測試
Redis伺服器以周期性事件的方式來運行serverCron函數,在伺服器運行期間,每隔一段時間, serverCron就會執行一次,直到伺服器關閉為止。在Redis2.6版本,伺服器預設規定serverCron每秒運行10次,平均每間隔100毫秒運行一次。從Redis2.8開始,使用者可以通過修改hz選項來調整serverCron的每秒執行次數,具體資訊請參考樣本設定檔redis.conf關於hz選項的說明
事件的調度與執行
因為伺服器同時存在檔案事件和時間事件兩種事件類型,所以伺服器必須對兩種事件進行調度,決定何時處理檔案事件,何時處理時間事件,以及花多少時間來處理它們等等。事件的調度和執行由ae.c/aeProcessEvents函數負責。可以用以下原始碼完成:
def aeProcessEvents(): #擷取到達時間離目前時間最接近的時間事件 time_event = aeSearchNearestTimer() #計算最接近的時間事件距離到達還有多少毫秒 remaind_ms = time_event.when - unix_ts_now() #如果事件已到達,那麼remaind_ms的值可能為負數,將它設定為0 if remaind_ms < 0: remaind_ms = 0 #根據remaind_ms的值,建立timeval結構 timeval = create_timeval_with_ms(remaind_ms) #阻塞並等待檔案事件產生,最大阻塞時間由傳入的timeval結構決定 #如果remaind_ms的值為0,那麼aeApiPoll調用之後馬上返回,不阻塞 aeApiPoll(timeval) #處理所有已產生的檔案事件(其實並沒有這個函數) processFileEvents() #處理所有已到達的時間事件 processTimeEvents()
Redis實現之事件