目錄
一、事件
1、文件事件
讀事件
寫事件
2、 時間事件
3、時間事件應用實例:服務器常規操作
4、事件的執行與調度
5、事件是否有重要性級別或優先級?需要立即處理還是可以延遲處理?
6、事件的類型是什么?是針對鍵的操作,還是集群的狀態變化?
二、小結
一、事件
事件是 Redis 服務器的核心,它處理兩項重要的任務:
-
處理文件事件:在多個客戶端中實現多路復用,接受它們發來的命令請求,并將命令的執行結果返回給客戶端。
-
時間事件:實現服務器常規操作(server cron job)。
本文以下內容就來介紹這兩種事件,以及它們背后的運作模式。
1、文件事件
Redis 服務器通過在多個客戶端之間進行多路復用,從而實現高效的命令請求處理:多個客戶 端通過套接字連接到 Redis 服務器中,但只有在套接字可以無阻塞地進行讀或者寫時,服務器 才會和這些客戶端進行交互。
Redis 將這類因為對套接字進行多路復用而產生的事件稱為文件事件(file event),文件事件可 以分為讀事件和寫事件兩類。
讀事件
讀事件標志著客戶端命令請求的發送狀態。
當一個新的客戶端連接到服務器時,服務器會給為該客戶端綁定讀事件,直到客戶端斷開連接 之后,這個讀事件才會被移除。讀事件在整個網絡連接的生命期內,都會在等待和就緒兩種狀態之間切換:
? 當客戶端只是連接到服務器,但并沒有向服務器發送命令時,該客戶端的讀事件就處于 等待狀態。
? 當客戶端給服務器發送命令請求,并且請求已到達時(相應的套接字可以無阻塞地執行讀 操作),該客戶端的讀事件處于就緒狀態。作為例子,下圖展示了三個已連接到服務器、但并沒有發送命令的客戶端:
?這三個客戶端的狀態如下表:
之后,當客戶端 X 向服務器發送命令請求,并且命令請求已到達時,客戶端 X 的讀事件狀態 變為就緒:
?這時,三個客戶端的狀態如下表(只有客戶端 X 的狀態被更新了):
當事件處理器被執行時,就緒的文件事件會被識別到,相應的命令請求會被發送到命令執行 器,并對命令進行求值。
寫事件
寫事件標志著客戶端對命令結果的接收狀態。
和客戶端自始至終都關聯著讀事件不同,服務器只會在有命令結果要傳回給客戶端時,才會為 客戶端關聯寫事件,并且在命令結果傳送完畢之后,客戶端和寫事件的關聯就會被移除。
一個寫事件會在兩種狀態之間切換:
? 當服務器有命令結果需要返回給客戶端,但客戶端還未能執行無阻塞寫,那么寫事件處 于等待狀態。
? 當服務器有命令結果需要返回給客戶端,并且客戶端可以進行無阻塞寫,那么寫事件處 于就緒狀態。
當客戶端向服務器發送命令請求,并且請求被接受并執行之后,服務器就需要將保存在緩存內 的命令執行結果返回給客戶端,這時服務器就會為客戶端關聯寫事件。
作為例子,下圖展示了三個連接到服務器的客戶端,其中服務器正等待客戶端 X 變得可寫,從 而將命令的執行結果返回給它:
?此時三個客戶端的事件狀態分別如下表:
當客戶端 X 的套接字可以進行無阻塞寫操作時,寫事件就緒,服務器將保存在緩存內的命令執 行結果返回給客戶端:?此時三個客戶端的事件狀態分別如下表(只有客戶端 X 的狀態被更新了):
當命令執行結果被傳送回客戶端之后,客戶端和寫事件之間的關聯會被解除(只剩下讀事件), 至此,返回命令執行結果的動作執行完畢:?
Note: 同時關聯寫事件和讀事件
前面提到過,讀事件只有在客戶端斷開和服務器的連接時,才會被移除。
這也就是說,當客戶端關聯寫事件的時候,實際上它在同時關聯讀/寫兩種事件。
因為在同一次文件事件處理器的調用中,單個客戶端只能執行其中一種事件(要么讀,要么寫, 但不能又讀又寫),當出現讀事件和寫事件同時就緒的情況時,事件處理器優先處理讀事件。
這也就是說,當服務器有命令結果要返回客戶端,而客戶端又有新命令請求進入時,服務器先 處理新命令請求。
2、 時間事件
時間事件記錄著那些要在指定時間點運行的事件,多個時間事件以無序鏈表的形式保存在服務器狀態中。 每個時間事件主要由三個屬性組成:
? when :以毫秒格式的 UNIX 時間戳為單位,記錄了應該在什么時間點執行事件處理函數。
? timeProc :事件處理函數。
? next 指向下一個時間事件,形成鏈表。
根據 timeProc 函數的返回值,可以將時間事件劃分為兩類:
-
如果事件處理函數返回ae.h/AE_NOMORE,那么這個事件為單次執行事件:該事件會在指 定的時間被處理一次,之后該事件就會被刪除,不再執行。
-
如果事件處理函數返回一個非AE_NOMORE的整數值,那么這個事件為循環執行事件:該 事件會在指定的時間被處理,之后它會按照事件處理函數的返回值,更新事件的 when 屬 性,讓這個事件在之后的某個時間點再次運行,并以這種方式一直更新并運行下去。可以用偽代碼來表示這兩種事件的處理方式:
def handle_time_event(server, time_event):# 執行事件處理器,并獲取返回值# 返回值可以是 AE_NOMORE ,或者一個表示毫秒數的非符整數值 retval = time_event.timeProc()if retval == AE_NOMORE:# 如果返回 AE_NOMORE ,那么將事件從鏈表中刪除,不再執行server.time_event_linked_list.delete(time_event) else:# 否則,更新事件的 when 屬性# 讓它在當前時間之后的 retval 毫秒之后再次運行 time_event.when = unix_ts_in_ms() + retval
當時間事件處理器被執行時,它遍歷所有鏈表中的時間事件,檢查它們的到達事件(when 屬 性),并執行其中的已到達事件:
def process_time_event(server): # 遍歷時間事件鏈表for time_event in server.time_event_linked_list: # 檢查事件是否已經到達if time_event.when >= unix_ts_in_ms():# 處理已到達事件 handle_time_event(server, time_event)
Note: 無序鏈表并不影響時間事件處理器的性能
在目前的版本中,正常模式下的 Redis 只帶有 serverCron 一個時間事件,而在 benchmark 模式下,Redis 也只使用兩個時間事件。 在這種情況下,程序幾乎是將無序鏈表退化成一個指針來使用,所以使用無序鏈表來保存時間事件,并不影響事件處理器的性能。
3、時間事件應用實例:服務器常規操作
對于持續運行的服務器來說,服務器需要定期對自身的資源和狀態進行必要的檢查和整理,從而讓服務器維持在一個健康穩定的狀態,這類操作被統稱為常規操作(cron job)。 在 Redis 中,常規操作由 redis.c/serverCron 實現,它主要執行以下操作:
? 更新服務器的各類統計信息,比如時間、內存占用、數據庫占用情況等。
? 清理數據庫中的過期鍵值對。
? 對不合理的數據庫進行大小調整。
? 關閉和清理連接失效的客戶端。
? 嘗試進行 AOF 或 RDB 持久化操作。
? 如果服務器是主節點的話,對附屬節點進行定期同步。
? 如果處于集群模式的話,對集群進行定期同步和連接測試。
Redis 將 serverCron 作為時間事件來運行,從而確保它每隔一段時間就會自動運行一次,又 因為 serverCron 需要在 Redis 服務器運行期間一直定期運行,所以它是一個循環時間事件: serverCron 會一直定期執行,直到服務器關閉為止。
在 Redis 2.6 版本中,程序規定 serverCron 每隔 10 毫秒就會被運行一次。從 Redis 2.8 開始, 10 毫秒是 serverCron 運行的默認間隔,而具體的間隔可以由用戶自己調整。
4、事件的執行與調度
既然 Redis 里面既有文件事件,又有時間事件,那么如何調度這兩種事件就成了一個關鍵問題。 簡單地說,Redis 里面的兩種事件呈合作關系,它們之間包含以下三種屬性:
-
一種事件會等待另一種事件執行完畢之后,才開始執行,事件之間不會出現搶占。
-
事件處理器先處理文件事件(處理命令請求),再執行時間事件(調用serverCron)
-
文件事件的等待時間(類poll函數的最大阻塞時間),由距離到達時間最短的時間事件 決定。
這些屬性表明,實際處理時間事件的時間,通常會比時間事件所預定的時間要晚,至于延遲的 時間有多長,取決于時間事件執行之前,執行文件事件所消耗的時間。
比如說,以下圖表就展示了,雖然時間事件 TE 1 預定在 t1 時間執行,但因為文件事件 FE 1 正在運行,所以 TE 1 的執行被延遲了:
另外,對于像 serverCron 這類循環執行的時間事件來說,如果事件處理器的返回值是 t ,那 么 Redis 只保證:
-
如果兩次執行時間事件處理器之間的時間間隔大于等于t,那么這個時間事件至少會被 處理一次。
-
而并不是說,每隔 t 時間,就一定要執行一次事件——這對于不使用搶占調度的 Redis 事件處理器來說,也是不可能做到的
舉個例子,雖然 serverCron (sC)設定的間隔為 10 毫秒,但它并不是像如下那樣每隔 10 毫
秒就運行一次:
在實際中,serverCron 的運行方式更可能是這樣子的:?
根據情況,如果處理文件事件耗費了非常多的時間,serverCron 被推遲到一兩秒之后才能執 行,也是有可能的。整個事件處理器程序可以用以下偽代碼描述:
def process_event():# 獲取執行時間最接近現在的一個時間事件te = get_nearest_time_event(server.time_event_linked_list)# 檢查該事件的執行時間和現在時間之差# 如果值 <= 0 ,那么說明至少有一個時間事件已到達 # 如果值 > 0 ,那么說明目前沒有任何時間事件到達 nearest_te_remaind_ms = te.when - now_in_ms()if nearest_te_remaind_ms <= 0:# 如果有時間事件已經到達# 那么調用不阻塞的文件事件等待函數 poll(timeout=None)else:# 如果時間事件還沒到達# 那么阻塞的最大時間不超過 te 的到達時間 poll(timeout=nearest_te_remaind_ms)# 處理已就緒文件事件 process_file_events()# 處理已到達時間事件 process_time_event()
通過這段代碼,可以清晰地看出:
? 到達時間最近的時間事件,決定了poll的最大阻塞時長。 ? 文件事件先于時間事件處理。
將這個事件處理函數置于一個循環中,加上初始化和清理函數,這就構成了 Redis 服務器的主 函數調用:
def redis_main(): # 初始化服務器init_server()# 一直處理事件,直到服務器關閉為止 while server_is_not_shutdown():process_event()# 清理服務器 clean_server()
5、事件是否有重要性級別或優先級?需要立即處理還是可以延遲處理?
Redis事件沒有定義重要性級別或優先級。所有的事件都會按照發生的順序進行處理,沒有特定的順序要求。因此,Redis事件可以根據需要進行立即處理,也可以延遲處理。
6、事件的類型是什么?是針對鍵的操作,還是集群的狀態變化?
Redis事件的類型有兩種,分別是針對鍵的操作事件和集群的狀態變化事件。
-
針對鍵的操作事件(Key Event):這些事件與鍵的操作相關,包括以下幾種類型:
- SET:當一個鍵被設置或修改時觸發。
- GET:當一個鍵被獲取時觸發。
- DEL:當一個鍵被刪除時觸發。
- EXPIRE:當一個鍵的過期時間被設置時觸發。
- EXPIRED:當一個鍵的過期時間到達時觸發。
- RENAME:當一個鍵被重命名時觸發。
- PERSIST:當一個鍵的過期時間被移除時觸發。
- ...
-
集群的狀態變化事件(Cluster Event):這些事件與Redis集群的狀態變化相關,包括以下幾種類型:
- NODE ADDED:當一個新的節點加入集群時觸發。
- NODE REMOVED:當一個節點被移除集群時觸發。
- NODE UPDATED:當一個節點的狀態或信息發生變化時觸發。
- SLOT ASSIGNED:當一個槽位被指派給一個節點時觸發。
- SLOT UNASSIGNED:當一個槽位從一個節點上移除時觸發。
- ...?
二、小結
-
Redis 的事件分為時間事件和文件事件兩類。
-
文件事件分為讀事件和寫事件兩類:讀事件實現了命令請求的接收,寫事件實現了命令 結果的返回。
-
時間事件分為單次執行事件和循環執行事件,服務器常規操作serverCron就是循環事 件。
-
文件事件和時間事件之間是合作關系:一種事件會等待另一種事件完成之后再執行,不 會出現搶占情況。
-
時間事件的實際執行時間通常會比預定時間晚一些。