Redis筆記(10):事件

yaluoshan發表於2020-11-15

Redis伺服器是一個事件驅動程式,伺服器需要處理檔案事件、時間事件兩類事件。

檔案事件(file event): Redis伺服器通過套接字與客戶端(可以是其他Redis伺服器)進行連線,檔案事件就是伺服器對套接字操作的抽象。伺服器與客戶端的通訊會產生相應的檔案事件,而伺服器則通過監聽並處理這些事件來完成一系列網路通訊操作。

時間事件(time event): Redis伺服器中的一些操作(比如serverCron函式)需要在給定的時間點執行,而時間事件就是伺服器對這類定時操作的抽象。

1.檔案事件

Redis基於Reactor模式開發了自己的網路事件處理器:這個處理器被稱為檔案事件處理器(file event handler):

  • 檔案事件處理器以單執行緒方式執行,使用ⅠO多路複用程式來同時監聽多個套接字,既實現了高效能的網路通訊模型,又可以很好地與Redis伺服器中其他同樣以單執行緒方式執行的模組進行對接,這保持了Redis內部單執行緒設計的簡單性。
  • 當被監聽的套接字準備好執行連線應答(accept)、讀取(read)、寫人(write)、關閉(close)等操作時,與操作相對應的檔案事件就會產生,這時檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件。

(1)檔案事件處理器的構成

檔案事件處理器的四個組成部分,它們分別是套接字socket、IO多路複用程式、檔案事件分派器(dispatcher),以及事件處理器套接字。

img

套接字: 套接字操作可以抽象為檔案事件,每當一個套接字準備好執行連線應答、寫入、讀取、關閉等操作時,就會產生一個檔案事件。一個伺服器通常會連線多個套接字,因此多個檔案事件有可能會併發地出現。

IO多路複用程式: 負責監聽多個套接字,並向檔案事件分派器傳送那些產生了事件的套接字。

檔案事件分派器: 接收IO多路複用程式傳來的套接字,並根據套接字產生的事件的型別,呼叫相應的事件處理器。

事件處理器: 伺服器會為執行不同任務的套接字關聯不同的事件處理器,事件處理器是一個函式,定義了某個事件發生時,伺服器應該執行的動作。

儘管多個檔案事件可能會併發地出現,但IO多路複用程式會將所有產生事件的套接字都放到一個佇列裡面,然後通過這個佇列,以有序、同步、每次一個套接字的方式向檔案事件分派器傳送套接字。當上一個套接字產生的事件被處理完畢之後(該套接字為事件所關聯的事件處理器執行完畢),IO多路複用程式才會繼續向檔案事件分派器傳送下一個套接字,如下圖所示。

img

(2)I/O多路複用函式的實現

Redis的I/O多路複用程式的所有功能都是通過包裝常見的select、epoll、evport和kqueue這些I/O多路複用函式庫來實現的,每個I/O多路複用函式庫在Redis原始碼中都對應一個單獨的檔案。因為Redis為每個IO多路複用函式庫都實現了相同的API,所以IO多路複用程式的底層實現是可以互換的。

(3)事件的型別

I/O多路複用程式可以監聽多個套接字的ae.h/AEREADABLE事件和ae.h/AEWRITABLE事件,這兩類事件和套接字操作之間的對應關係如下:

  • AEREADABLE事件: 當套接字變得可讀時(客戶端對套接字執行write操作,伺服器可以讀取套接字),或者有新的可應答(acceptable)套接字出現時(客戶端對伺服器的監聽套接字執行connect操作),套接字產生AEREADABLE事件。
  • AEWRITABLE事件: 當套接字變得可寫時(伺服器向套接字寫,客戶端可對套接字執行read操作),套接字產生AEWRITABLE事件。

I/O多路複用程式允許伺服器同時監聽套接字的AEREADABLE事件和AEWRITABLE事件,如果一個套接字同時產生了這兩種事件,那麼檔案事件分派器會優先處理AEREADABLE事件,等到AEREADABLE事件處理完之後,才處理AEWRITABLE事件。

(4)檔案事件處理器

Redis為檔案事件編寫了多個處理器,用於實現不同的網路通訊需求,比如如下處理器,在這些事件處理器裡面,伺服器最常用的要數與客戶端進行通訊的連線應答處理器、命令請求處理器和命令回覆處理器。

  • 連線應答處理器:對連線伺服器的各個客戶端進行應答;
  • 命令請求處理器:接收客戶端傳來的命令請求;
  • 命令回覆處理器向客戶端返回命令的執行結果;
  • 複製處理器:處理主伺服器和從伺服器進行復制操作;

①連線應答處理器

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

當Redis伺服器進行初始化的時候,程式會將這個連線應答處理器和伺服器監聽套接字的AE READABLE事件關聯起來,當有客戶端用sys/socket.h/connect函式連線伺服器監聽套接字的時候,套接字就會產生AEREADABLE事件,引發連線應答處理器執行,並執行相應的套接字應答操作。

②命令請求處理器

networking.c/readQueryFromClient函式是Redis的命令請求處理器,這個處理器負責從套接字中讀入客戶端傳送的命令請求內容,具體實現為unistd.h/read函式的包裝。

當一個客戶端通過連線應答處理器成功連線到伺服器之後,伺服器會將客戶端套接字的AEREADABLE事件和命令請求處理器關聯起來,當客戶端向伺服器傳送命令請求的時候,套接字就會產生AEREADABLE事件,引發命令請求處理器執行,並執行相應的套接字讀入操作。

在客戶端連線伺服器的整個過程中,伺服器都會一直為客戶端套接字的AEREADABLE事件關聯命令請求處理器。

③命令回覆處理器

networking.c/sendReplyToClient函式是Redis的命令回覆處理器,這個處理器負責將伺服器執行命令後得到的命令回覆通過套接字返回給客戶端,具體實現為unistd.h/write函式的包裝。

當伺服器有命令回覆需要傳送給客戶端的時候,伺服器會將客戶端套接字的AEWRITABLE事件和命令回覆處理器關聯起來,當客戶端準備好接收伺服器傳回的命令回覆時,就會產生AEWRITABLE事件,引發命令回覆處理器執行,並執行相應的套接字寫伺服器入操作。

當命令回覆傳送完畢之後,伺服器就會解除命令回覆處理器與客戶端套接字的AEWRITABLE事件之間的關聯。

(5)伺服器與客戶端連線、請求的完整過程

一個Redis伺服器正在運作,伺服器端的套接字監聽AEREADABLE事件,該事件關聯連線應答處理器。

這時有一個Redis客戶端向伺服器發起連線,那麼監聽套接字將產生AEREADABLE事件,觸發連線應答處理器執行。連線應答處理器會對客戶端的連線請求進行應答,然後建立一個客戶端套接字,以及客戶端狀態redisClient,並將客戶端套接字的AEREADABLE事件與命令請求處理器進行關聯,使得客戶端可以向主伺服器傳送命令請求。

然後,客戶端向伺服器傳送一個命令請求,那麼客戶端套接字將產生AEREADABLE事件,引發命令請求處理器執行,處理器讀取客戶端的命令內容,然後傳給相關程式去執行。

命令執行完畢後,伺服器會將客戶端套接字的AEWRITABLE事件與命令回覆處理器進行關聯,當客戶端嘗試讀取命令回覆的時候,客戶端套接字將產生AEWRITABLE事件,觸發命令回覆處理器執行,當命令回覆處理器將命令回覆全部寫人到套接字之後,伺服器就會解除客戶端套接字的AE WRITABLE事件與命令回覆處理器之間的關聯,等待下一次接收命令。

2.時間事件

(1)時間事件的組成

Redis的時間事件分為定時事件、週期事件兩類,定時事件讓程式在指定的時間之後執行一次。週期性事件讓程式每隔指定時間就執行一次。時間事件主要由以下三個屬性組成:

  • id: 伺服器為時間事件建立的全域性唯一ID,ID號按從小到大的順序遞增,新事件的ID號比舊事件的ID號要大。
  • when: 毫秒精度的UNX時間戳,記錄了時間事件的到達(arrive)時間。
  • timeproc: 時間事件處理器,一個函式。當時間事件到達時,伺服器就會呼叫相應的處理器來處理事件。

時間事件是定時事件還是週期性事件,取決於時間事件處理器的返回值:

  • 如果事件處理器返回ae.h/AENOMORE,那麼這個事件為定時事件,該事件在達到一次之後就會被刪除,之後不再到達。
  • 如果事件處理器返回一個非AENOMORE的整數值,那麼這個事件為週期性時間當一個時間事件到達之後,伺服器會根據事件處理器返回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間之後再次到達,並以這種方式一直更新並執行下去。

(2)時間事件的實現

伺服器將所有時間事件都放在一個無序連結串列(指不按時間排序)中,每當時間事件執行器執行時,它就遍歷整個連結串列,查詢所有已到達的時間事件,並呼叫相應的事件處理器。

因為連結串列沒有按when屬性進行排序,所以當時間事件執行器執行的時候,它必須遍歷連結串列中的所有時間事件,這樣才能確保伺服器中所有已到達的時間事件都會被處理。

img

無序連結串列並不影響時間事件處理器的效能,在目前版本中,正常模式下的Redis伺服器只使用 serverCron一個時間事件,而在benchmark模式下,伺服器也只使用兩個時間事件。在這種情況下,伺服器幾乎是將無序連結串列退化成一個指標來使用,所以使用無序連結串列來儲存時間事件,並不影響事件執行的效能。

(3)serverCorn函式

持續執行的Redis伺服器需要定期對自身的資源和狀態進行檢查和調整,從而確保伺服器可以長期、穩定地執行,這些定期操作由redis.c/serverCron函式負責執行,它的主要工作包括:

  • 更新伺服器的各類統計資訊,比如時間、記憶體佔用、資料庫佔用情況等。
  • 清理資料庫中的過期鍵值對。
  • 關閉和清理連線失效的客戶端。
  • 嘗試進行AOF或RDB持久化操作。
  • 如果伺服器是主伺服器,那麼對從伺服器進行定期同步。
  • 如果處於叢集模式,對叢集進行定期同步和連線測試。

在Redis2.6版本,伺服器預設規定servercron每秒執行10次,平均每間隔100毫秒執行一次。使用者可以通過修改hz選項來調整serverCron的每秒執行次數。

3.事件的排程與執行

伺服器中同時存在檔案事件和時間事件兩種事件型別,伺服器必須對這兩種事件進行排程,決定何時應該處理檔案事件,何時又應該處理時間事件,以及花多少時間來處理事件,事件的排程和執行由ae.c/aeProcessEvents函式負責,伺服器執行排程流程如下。

img

以下是事件的排程和執行規則:

  • aeApiPoll函式的最大阻塞時間由到達時間最接近當前時間的時間事件決定,這個方法既可以避免伺服器對時間事件進行頻繁的輪詢(忙等待),也可以確保aeApiPoll函式不會阻塞過長時間。
  • 檔案事件是隨機出現的,如果等待並處理完一次檔案事件之後,仍未有任何時間事件到達,那麼伺服器將再次等待並處理檔案事件。當檔案事件處理完畢時,發現有時間事件到達,這時伺服器開始處理到達的時間事件。
  • 對檔案事件和時間事件的處理都是同步、有序、原子地執行的,伺服器不會中途中斷事件處理,也不會對事件進行搶佔,因此,不管是檔案事件的處理器,還是時間事件的處理器,它們都會盡可地減少程式的阻塞時間,並在有需要時主動讓出執行權,從而降低造成事件飢餓俄的可能性。比如說,在命令回覆處理器將一個命令回覆寫人到客戶端套接字時,如果寫入位元組數超過了一個預設常量的話,命令回覆處理器就會主動用break跳出寫入迴圈,將餘下的資料留到下次再寫;另外,時間事件也會將非常耗時的持久化操作放到子執行緒或者子程式執行。
  • 時間事件在檔案事件之後執行,且事件之間不會出現搶佔,所以時間事件的實際處理時間,通常會比時間事件設定的到達時間稍晚一些。

相關文章