Redis之執行緒模型
阿新 • • 發佈:2021-11-28
1、前言
Redis4.0版本之後開始使用多執行緒,之前使用的是單執行緒。無論是使用單執行緒模型還是多執行緒模型,這兩個設計上的決定都是為了更好地提升 Redis 的開發效率、執行效能。雖然 Redis 在較新的版本中引入了多執行緒,不過是在部分命令上引入的,其中包括非阻塞的刪除操作,在整體的架構設計上,主處理程式還是單執行緒模型的。
2、執行緒模型
Redis 基於 Reactor 模式開發了自己的網路事件處理器: 這個處理器被稱為檔案事件處理器(file event handler),由套接字、I/O多路複用程式、檔案事件分派器(dispatcher),事件處理器四部分組成。
檔案事件處理器使用 I/O 多路複用(multiplexing)程式來同時監聽多個套接字, 並根據套接字目前執行的任務來為套接字關聯不同的事件處理器。
當被監聽的套接字準備好執行連線應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時, 與操作相對應的檔案事件就會產生, 這時檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件。
雖然檔案事件處理器以單執行緒方式執行, 但通過使用 I/O 多路複用程式來監聽多個套接字, 檔案事件處理器既實現了高效能的網路通訊模型, 又可以很好地與 redis 伺服器中其他同樣以單執行緒方式執行的模組進行對接, 這保持了 Redis 內部單執行緒設計的簡單性。
Redis服務端通過Socket與客戶端連線。每當一個套接字準備好執行連線應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,就會產生一個檔案事件。因為一個伺服器通常會連線多個套接字, 所以多個檔案事件有可能會併發地出現。比如,當socket變得可讀時,socket就會產生一個 AE_READABLE事件;當socket變得可寫的時候,socket會產生一個 AE_WRITABLE事件。
2.1、檔案事件處理器的構成
檔案事件處理器的四個組成部分, 它們分別是套接字、 I/O 多路複用程式、 檔案事件分派器(dispatcher)、 以及事件處理器。
檔案事件是對套接字操作的抽象, 每當一個套接字準備好執行連線應答(accept)、寫入、讀取、關閉等操作時, 就會產生一個檔案事件。 因為一個伺服器通常會連線多個套接字, 所以多個檔案事件有可能會併發地出現。
I/O 多路複用程式負責監聽多個套接字, 並向檔案事件分派器傳送那些產生了事件的套接字。
儘管多個檔案事件可能會併發地出現, 但 I/O 多路複用程式總是會將所有產生事件的套接字都入隊到一個佇列裡面, 然後通過這個佇列, 以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向檔案事件分派器傳送套接字: 當上一個套接字產生的事件被處理完畢之後(該套接字為事件所關聯的事件處理器執行完畢), I/O 多路複用程式才會繼續向檔案事件分派器傳送下一個套接字。
檔案事件分派器接收 I/O 多路複用程式傳來的套接字, 並根據套接字產生的事件的型別, 呼叫相應的事件處理器。伺服器會為執行不同任務的套接字關聯不同的事件處理器, 這些處理器是一個個函式, 它們定義了某個事件發生時, 伺服器應該執行的動作。
2.2、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 多路複用程式的底層實現是可以互換的, 如圖所示。
Redis 在 I/O 多路複用程式的實現原始碼中用 #include 巨集定義了相應的規則, 程式會在編譯時自動選擇系統中效能最高的 I/O 多路複用函式庫來作為 Redis 的 I/O 多路複用程式的底層實現:
/* 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 #endif2.3、事件的型別與排程 伺服器需要處理兩類事件:檔案事件,時間事件。 (1)檔案事件:Redis伺服器對套接字的操作,當一個套接字準備執行連線、讀、寫、關閉等操作時就會產生一個檔案事件。檔案事件分為AE_READABLE和AE_WRITABLE兩類。
- 當套接字變得可讀時(客戶端對套接字執行 write 操作,或者執行 close 操作), 或者有新的可應答(acceptable)套接字出現時(客戶端對伺服器的監聽套接字執行 connect 操作), 套接字產生AE_READABLE事件。
- 當套接字變得可寫時(客戶端對套接字執行 read 操作), 套接字產生AE_WRITABLE事件。
- 更新伺服器的各類統計資訊,如時間、記憶體佔用、資料庫佔用情況等。
- 清理資料庫中的過期鍵值對。
- 關閉和清理連線失效的客戶端。
- 嘗試進行AOF或RDB持久化操作。
- 如果伺服器是主伺服器,那麼對從伺服器進行定期同步。
- 如果處於叢集模式,對叢集進行定期執行同步和連線測試。
- Redis2.6版本,伺服器預設serverCron每秒執行10次,平均每隔100毫秒執行一次。Redis2.8版本之後,通過hz調整每秒執行次數。
- 如果套接字沒有任何事件被監聽, 那麼函式返回 AE_NONE 。
- 如果套接字的讀事件正在被監聽, 那麼函式返回 AE_READABLE 。
- 如果套接字的寫事件正在被監聽, 那麼函式返回 AE_WRITABLE 。
- 如果套接字的讀事件和寫事件正在被監聽, 那麼函式返回 AE_READABLE | AE_WRITABLE 。
- 為了對連線伺服器的各個客戶端進行應答, 伺服器要為監聽套接字關聯連線應答處理器。
- 為了接收客戶端傳來的命令請求, 伺服器要為客戶端套接字關聯命令請求處理器。
- 為了向客戶端返回命令的執行結果, 伺服器要為客戶端套接字關聯命令回覆處理器。
- 當主伺服器和從伺服器進行復制操作時, 主從伺服器都需要關聯特別為複製功能編寫的複製處理器。
- 注意1:只有當上一個套接字產生的事件被所關聯的事件處理器執行完畢,I/O多路複用程式才會繼續向檔案事件分派器傳送下一個套接字,所以對每個命令的執行時間是有要求的,如果某個命令執行過長,會造成其他命令的阻塞。所以慎用O(n)命令,Redis是面向快速執行場景的資料庫。
- 注意2:命令的併發性。Redis是單執行緒處理命令,命令會被逐個被執行,假如有3個客戶端命令同時執行,執行順序是不確定的,但能確定不會有兩條命令被同時執行,所以兩條incr命令無論怎麼執行最終結果都是2。
- 假設一個 Redis 伺服器正在運作, 那麼這個伺服器的監聽套接字的 AE_READABLE 事件應該正處於監聽狀態之下, 而該事件所對應的處理器為連線應答處理器。
- 如果這時有一個 Redis 客戶端向伺服器發起連線, 那麼監聽套接字將產生 AE_READABLE 事件, 觸發連線應答處理器執行: 處理器會對客戶端的連線請求進行應答, 然後建立客戶端套接字, 以及客戶端狀態, 並將客戶端套接字的 AE_READABLE 事件與命令請求處理器進行關聯, 使得客戶端可以向主伺服器傳送命令請求。
- 之後, 假設客戶端向主伺服器傳送一個命令請求, 那麼客戶端套接字將產生 AE_READABLE 事件, 引發命令請求處理器執行, 處理器讀取客戶端的命令內容, 然後傳給相關程式去執行。
- 執行命令將產生相應的命令回覆, 為了將這些命令回覆傳送回客戶端, 伺服器會將客戶端套接字的 AE_WRITABLE 事件與命令回覆處理器進行關聯: 當客戶端嘗試讀取命令回覆的時候, 客戶端套接字將產生 AE_WRITABLE 事件, 觸發命令回覆處理器執行, 當命令回覆處理器將命令回覆全部寫入到套接字之後, 伺服器就會解除客戶端套接字的 AE_WRITABLE 事件與命令回覆處理器之間的關聯。
//客戶端狀態的關鍵屬性: typedef struct redisClient { //…… sds querybuf; robj **argv; int argc; struct redisCommand *cmd; char buf[REDIS_REPLY_CHUNK_BYTES]; int bufpos; list *reply; //…… } redisClient;一個命令請求從傳送到獲得回覆的過程: 1、傳送命令請求: 當客戶端使用connect函式連線到伺服器時,伺服器就會呼叫連線事件處理器,為客戶端建立相應的客戶端狀態,並將這個新的客戶端狀態新增到伺服器狀態結構clients連結串列的末尾。 當用戶在客戶端中鍵入一個命令請求時,客戶端會將這個命令請求轉換成協議格式,然後通過連線到伺服器的套接字,將協議格式的命令請求傳送給伺服器。 2、讀取命令請求: 當客戶端與伺服器之間的連線套接字因為客戶端的寫入而變得可讀時,伺服器將呼叫命令請求處理器來執行以下操作:
- 讀取套接字中協議格式的命令請求,並將其儲存到客戶端狀態的輸入緩衝區querybuf。
- 對輸入緩衝區中的命令請求進行分析,提取出命令請求中包含的命令引數,以及命令引數的個數,然後分別將引數和引數個數儲存到客戶端狀態的argv和argc屬性。
- 呼叫命令執行器,執行客戶端指定的命令。
- 命令執行器要做的第一件事就是根據客戶端狀態的argv[0]引數,在命令表中查詢引數所指定的命令,並將找到的命令儲存到客戶端狀態的cmd屬性中。
- 到目前為止,伺服器已經將執行命令所需的命令實現函式(cmd)、引數(argv)、引數個數(argc)都收集齊了,但在真正執行命令前,程式還需要進行一些預備操作,從而確保命令可以正確、順利地被執行:檢查客戶端狀態的cmd指標是否執行NULL、檢查命令請求所給定的引數個數是否正確、檢查客戶端是否已經通過了身份驗證...
- 被呼叫的命令實現函式會執行指定的操作,併產生相應的命令回覆,這些回覆會被儲存在客戶端狀態的輸出緩衝區中(buf屬性和reply屬性),之後實現函式還會為客戶端的套接字關聯命令回覆處理器,這個處理器負責將命令回覆返回給客戶端。
- 如果伺服器開啟了慢查詢日誌功能,那麼慢查詢日誌模組會檢查是否需要為剛剛執行完的命令請求新增一條新的慢查詢日誌。
- 根據剛剛執行命令所耗費的時長,更新被執行命令的redisCommand結構的milliseconds屬性,並將命令的redisCommand結構的calls計數器的值加1。
- 如果伺服器開啟AOF持久化功能,那麼AOF持久化模組會將剛剛執行的命令請求寫入到AOF緩衝區。
- 如果有其他伺服器正在複製當前這個伺服器,那麼伺服器會將剛剛執行的命令傳播給所有從伺服器。
- 當客戶端套接字變為可寫狀態時,伺服器就會執行命令回覆處理器,將儲存在客戶端輸出緩衝區中的命令回覆傳送給客戶端。當命令回覆傳送完畢之後,回覆處理器會清空客戶端狀態的輸出緩衝區,為處理下一個命令請求做好準備。
- 當客戶端接收到協議格式的命令回覆之後,它會將這些回覆轉換成人們可以識別的可讀的格式。
- 如果使用者為這些屬性的相應選項指定了新的值,那麼伺服器就使用使用者指定的值來更新相應的屬性。
- 如果使用者沒有為屬性的相應選項設定新值,那麼伺服器就沿用為initServerConfig函式為屬性設定的預設值。
- 如果伺服器啟用了AOF持久化功能,那麼伺服器使用AOF檔案來還原資料庫狀態
- 相反地,如果伺服器沒有啟用AOF持久化功能,那麼伺服器使用RDB檔案來還原資料庫狀態