天天看點

Redis事件處理機制詳解(下)2 時間事件3 兩種事件的排程

2 時間事件

時間事件記錄着那些要在指定時間點運作的事件,多個時間事件以無序連結清單結構儲存在伺服器狀态中。

無序連結清單并不影響時間事件處理器的性能。

在Redis3.0版本,正常模式下的 Redis 隻帶有 serverCron 一個時間事件, 而在 benchmark 模式下, Redis 也隻使用兩個時間事件。

在這種情況下, 程式幾乎是将無序連結清單退化成一個指針來使用, 是以使用無序連結清單來儲存時間事件, 并不影響事件處理器性能。

時間事件的資料結構

Redis事件處理機制詳解(下)2 時間事件3 兩種事件的排程

根據

timeProc

函數傳回值,将時間事件分類如下:

  • 傳回 AE_NOMORE
  • Redis事件處理機制詳解(下)2 時間事件3 兩種事件的排程
  • 那麼這個事件為單次執行事件。該事件會在指定時間被處理一次,之後該事件就會被删除

傳回一個非 AE_NOMORE 的整數值,則為循環執行事件。該事件會在指定時間被處理,之後它會按照timeProc的傳回值,更新事件的 when 屬性,讓這個事件在之後某時間點再運作,以這種方式一直更新運作。

僞代碼表示的兩種事件處理:

def handle_time_event(server, time_event):
    # 執行事件處理器,并擷取傳回值
    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)      

時間事件執行個體

伺服器需要定期對自身的資源和狀态進行檢查、整理, 保證伺服器維持在一個健康穩定狀态, 這類操作被統稱為正常操作(cron job)。

在 Redis 中, 正常操作由

redis.c/serverCron

實作, 包括如下操作:

更新伺服器的各類統計資訊,比如時間、記憶體占用、資料庫占用情況等

清理資料庫中的過期鍵值對

對不合理的資料庫進行大小調整

關閉和清理連接配接失效的用戶端

嘗試進行 AOF 或 RDB 持久化操作

如果伺服器是主節點的話,對附屬節點進行定期同步

如果處于叢集模式的話,對叢集進行定期同步和連接配接測試

Redis 将 serverCron(後文簡稱為sC) 作為時間事件運作, 確定它能夠定期自動運作一次,又因 sC 需要在 Redis 伺服器運作期一直定期運作, 是以它是一個循環時間事件:sC 會一直定期執行,直至伺服器關閉。

Redis 2.6 的 sC 每秒運作 10 次,即平均每 100 ms運作一次。

Redis 2.8 使用者可以通過修改 hz 選項設定 sC 的每秒執行次數。

3 兩種事件的排程

簡單地說, Redis 裡面的兩種事件呈協作關系, 它們之間包含如下屬性:

  • 一種事件會等待另一種事件執行完後,才開始執行,事件之間不會出現搶占
  • 事件處理器先處理檔案事件(即處理指令請求),再執行時間事件(調用 sC)

檔案事件的等待時間(類 poll 函數的最大阻塞時間),由距離到達時間最短的時間事件決定

這表明, 實際處理時間事件的時間, 通常會比事件所預定的時間要晚, 延遲時間取決于時間事件執行前, 執行完成檔案事件所耗時間。

示例

正常案例

雖然時間事件 Time Event Y 可設定其

when

屬性計劃在

t1

時間執行, 但因為檔案事件 File Event X 正在運作, 是以 Time Event Y 的執行被延遲。

Redis事件處理機制詳解(下)2 時間事件3 兩種事件的排程

sC 案例

而且對于 sC 這類循環執行的時間事件來說,如果事件處理器的傳回值是 t ,那麼 Redis 隻保證:

如果兩次執行時間事件處理器之間的時間間隔≥t ,則該時間事件至少會被處理一次

而非,每隔 t 時間,就一定要執行一次事件

這對于不使用搶占排程的 Redis 事件處理器而言,也不可能做到

比如,雖然 sC 設定的間隔為 10 ms,但它并非是如下那樣每隔 10 ms就運作一次:

Redis事件處理機制詳解(下)2 時間事件3 兩種事件的排程

實際的 sC 運作方式更可能如下:

Redis事件處理機制詳解(下)2 時間事件3 兩種事件的排程

根據情況,如果處理檔案事件耗費了非常多的時間,sC 被推遲到一兩秒之後才能執行,也有可能。

整個事件處理器程式可以用以下僞代碼描述:

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()