Redis 的單線程模型


參考:https://www.cnblogs.com/barrywxx/p/8570821.html

 

Redis 內部使用文件事件處理器 file event handler ,這個文件事件處理器是單線程的,所以 Redis 才叫做單線程的模型。它采用 IO 多路復用機制同時監聽多個 socket,將產生事件的 socket 壓入內存隊列中,事件分派器根據 socket 上的事件類型來選擇對應的事件處理器進行處理。

文件事件處理器的結構包含 4 個部分:

  • 多個 socket
  • IO 多路復用程序
  • 文件事件分派器
  • 事件處理器(連接應答處理器、命令請求處理器、命令回復處理器)

多個 socket 可能會並發產生不同的操作,每個操作對應不同的文件事件,但是 IO 多路復用程序會監聽多個 socket,會將產生事件的 socket 放入隊列中排隊,事件分派器每次從隊列中取出一個 socket,根據 socket 的事件類型交給對應的事件處理器進行處理。

Redis 服務端進程初始化的時候,會將 server socket 的 AE_READABLE 事件與連接應答處理器關聯。

客戶端 socket01 向 Redis 進程的 server socket 請求建立連接,此時 server socket 會產生一個 AE_READABLE 事件,IO 多路復用程序監聽到 server socket 產生的事件后,將該 socket 壓入隊列中。文件事件分派器從隊列中獲取 socket,交給連接應答處理器。連接應答處理器會創建一個能與客戶端通信的 socket01,並將該 socket01 的 AE_READABLE 事件與命令請求處理器關聯。

假設此時客戶端發送了一個 set key value 請求,此時 Redis 中的 socket01 會產生 AE_READABLE 事件,IO 多路復用程序將 socket01 壓入隊列,此時事件分派器從隊列中獲取到 socket01 產生的 AE_READABLE 事件,由於前面 socket01 的 AE_READABLE 事件已經與命令請求處理器關聯,因此事件分派器將事件交給命令請求處理器來處理。命令請求處理器讀取 socket01 的 key value 並在自己內存中完成 key value 的設置。操作完成后,它會將 socket01 的 AE_WRITABLE 事件與命令回復處理器關聯。

如果此時客戶端准備好接收返回結果了,那么 Redis 中的 socket01 會產生一個 AE_WRITABLE 事件,同樣壓入隊列中,事件分派器找到相關聯的命令回復處理器,由命令回復處理器對 socket01 輸入本次操作的一個結果,比如 ok ,之后解除 socket01 的 AE_WRITABLE 事件與命令回復處理器的關聯。

這樣便完成了一次通信。關於 Redis 的一次通信過程,推薦讀者閱讀《Redis 設計與實現——黃健宏》進行系統學習。

 

文件事件處理器的構成

圖 IMAGE_CONSTRUCT_OF_FILE_EVENT_HANDLER 展示了文件事件處理器的四個組成部分, 它們分別是套接字、 I/O 多路復用程序、 文件事件分派器(dispatcher)、 以及事件處理器。

digraph {    label = "\n 圖 IMAGE_CONSTRUCT_OF_FILE_EVENT_HANDLER    文件事件處理器的四個組成部分";    rankdir = LR;    node [shape = box];    subgraph cluster_sockets {        style = dashed        label = "套接字";        c1 [label = "s1", shape = circle];        c2 [label = "s2", shape = circle];        other_client [label = "...", width = 1.1, shape = plaintext];        c3 [label = "sN", shape = circle];    }    io_multiplexing [label = "I\n/\nO\n多\n路\n復\n用\n程\n序"];    file_event_processor [label = "文\n件\n事\n件\n分\n派\n器"];    subgraph cluster_handlers {        style = dashed        label = "事件處理器";        write_handler [label = "命令請求處理器"];        read_handler [label = "命令回復處理器"];        connect_handler [label = "連接應答處理器"];        other_handlers [label = "...", width = 1.6];    }    c1 -> io_multiplexing;    c2 -> io_multiplexing;    other_client -> io_multiplexing [style = invis];    c3 -> io_multiplexing;    io_multiplexing -> file_event_processor;    file_event_processor -> write_handler;    file_event_processor -> read_handler;    file_event_processor -> connect_handler;    file_event_processor -> other_handlers;}

文件事件是對套接字操作的抽象, 每當一個套接字准備好執行連接應答(accept)、寫入、讀取、關閉等操作時, 就會產生一個文件事件。 因為一個服務器通常會連接多個套接字, 所以多個文件事件有可能會並發地出現。

I/O 多路復用程序負責監聽多個套接字, 並向文件事件分派器傳送那些產生了事件的套接字。

盡管多個文件事件可能會並發地出現, 但 I/O 多路復用程序總是會將所有產生事件的套接字都入隊到一個隊列里面, 然后通過這個隊列, 以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向文件事件分派器傳送套接字: 當上一個套接字產生的事件被處理完畢之后(該套接字為事件所關聯的事件處理器執行完畢), I/O 多路復用程序才會繼續向文件事件分派器傳送下一個套接字, 如圖 IMAGE_DISPATCH_EVENT_VIA_QUEUE 。

digraph {    rankdir = LR;    node [shape = record];    label = "\n圖 IMAGE_DISPATCH_EVENT_VIA_QUEUE    I/O 多路復用程序通過隊列向文件事件分派器傳送套接字";    //    subgraph cluster_io_multiplexing {        //style = dashed        label = "隊列";        queue [label = " { 套接字 sN | 套接字 sN-1 | ... | 套接字 s3 | 套接字 s2 } "];    }    file_event_processor [label = "文\n件\n事\n件\n分\n派\n器"];    //    queue -> file_event_processor [label = "傳送\n 套接字 s1", style = dashed];}

文件事件分派器接收 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 多路復用程序的底層實現是可以互換的, 如圖 IMAGE_MULTI_LIB 所示。

digraph {    label = "圖 IMAGE_MULTI_LIB    Redis 的 I/O 多路復用程序有多個 I/O 多路復用庫實現可選";    node [shape = box];    io_multiplexing [label = "I/O 多路復用程序"];    subgraph cluster_imp {        style = dashed        label = "底層實現";        labelloc = "b";        kqueue [label = "kqueue"];        evport [label = "evport"];        epoll [label = "epoll"];        select [label = "select"];    }    //    edge [dir = back];    io_multiplexing -> select;    io_multiplexing -> epoll;    io_multiplexing -> evport;    io_multiplexing -> kqueue;}

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 #endif 

事件的類型

I/O 多路復用程序可以監聽多個套接字的 ae.h/AE_READABLE 事件和 ae.h/AE_WRITABLE 事件, 這兩類事件和套接字操作之間的對應關系如下:

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

I/O 多路復用程序允許服務器同時監聽套接字的 AE_READABLE 事件和 AE_WRITABLE 事件, 如果一個套接字同時產生了這兩種事件, 那么文件事件分派器會優先處理 AE_READABLE 事件, 等到 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 多路復用函數庫的名稱: 返回 "epoll" 表示底層為 epoll 函數庫, 返回"select" 表示底層為 select 函數庫, 諸如此類。

文件事件的處理器

Redis 為文件事件編寫了多個處理器, 這些事件處理器分別用於實現不同的網絡通訊需求, 比如說:

  • 為了對連接服務器的各個客戶端進行應答, 服務器要為監聽套接字關聯連接應答處理器。
  • 為了接收客戶端傳來的命令請求, 服務器要為客戶端套接字關聯命令請求處理器。
  • 為了向客戶端返回命令的執行結果, 服務器要為客戶端套接字關聯命令回復處理器。
  • 當主服務器和從服務器進行復制操作時, 主從服務器都需要關聯特別為復制功能編寫的復制處理器。
  • 等等。

在這些事件處理器里面, 服務器最常用的要數與客戶端進行通信的連接應答處理器、 命令請求處理器和命令回復處理器。

連接應答處理器

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

當 Redis 服務器進行初始化的時候, 程序會將這個連接應答處理器和服務器監聽套接字的 AE_READABLE 事件關聯起來, 當有客戶端用sys/socket.h/connect 函數連接服務器監聽套接字的時候, 套接字就會產生 AE_READABLE 事件, 引發連接應答處理器執行, 並執行相應的套接字應答操作, 如圖 IMAGE_SERVER_ACCEPT_CONNECT 所示。

digraph {    label = "\n圖 IMAGE_SERVER_ACCEPT_CONNECT    服務器對客戶端的連接請求進行應答";    rankdir = LR;    client [label = "客戶端", shape = circle];    server [label = "服務器\n\n\n服務器監聽套接字產生\nAE_READABLE 事件\n執行連接應答處理器", shape = box, height = 2];    client -> server [label = "連接監聽套接字"];}

命令請求處理器

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

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

digraph {    label = "\n圖 IMAGE_SERVER_RECIVE_COMMAND_REQUEST    服務器接收客戶端發來的命令請求";    rankdir = LR;    client [label = "客戶端", shape = circle];    server [label = "服務器\n\n\n客戶端套接字產生\nAE_READABLE 事件\n執行命令請求處理器", shape = box, height = 2];    client -> server [label = "發送命令請求"];}

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

命令回復處理器

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

當服務器有命令回復需要傳送給客戶端的時候, 服務器會將客戶端套接字的 AE_WRITABLE 事件和命令回復處理器關聯起來, 當客戶端准備好接收服務器傳回的命令回復時, 就會產生 AE_WRITABLE 事件, 引發命令回復處理器執行, 並執行相應的套接字寫入操作, 如圖 IMAGE_SERVER_SEND_REPLY 所示。

digraph {    label = "\n圖 IMAGE_SERVER_SEND_REPLY    服務器向客戶端發送命令回復";    rankdir = LR;    client [label = "客戶端", shape = circle];    server [label = "服務器\n\n\n客戶端套接字產生\nAE_WRITABLE 事件\n執行命令回復處理器", shape = box, height = 2];    client -> server [dir = back, label = "發送命令回復"];}

當命令回復發送完畢之后, 服務器就會解除命令回復處理器與客戶端套接字的 AE_WRITABLE 事件之間的關聯。

一次完整的客戶端與服務器連接事件示例

讓我們來追蹤一次 Redis 客戶端與服務器進行連接並發送命令的整個過程, 看看在過程中會產生什么事件, 而這些事件又是如何被處理的。

假設一個 Redis 服務器正在運作, 那么這個服務器的監聽套接字的 AE_READABLE 事件應該正處於監聽狀態之下, 而該事件所對應的處理器為連接應答處理器。

如果這時有一個 Redis 客戶端向服務器發起連接, 那么監聽套接字將產生 AE_READABLE 事件, 觸發連接應答處理器執行: 處理器會對客戶端的連接請求進行應答, 然后創建客戶端套接字, 以及客戶端狀態, 並將客戶端套接字的 AE_READABLE 事件與命令請求處理器進行關聯, 使得客戶端可以向主服務器發送命令請求。

之后, 假設客戶端向主服務器發送一個命令請求, 那么客戶端套接字將產生 AE_READABLE 事件, 引發命令請求處理器執行, 處理器讀取客戶端的命令內容, 然后傳給相關程序去執行。

執行命令將產生相應的命令回復, 為了將這些命令回復傳送回客戶端, 服務器會將客戶端套接字的 AE_WRITABLE 事件與命令回復處理器進行關聯: 當客戶端嘗試讀取命令回復的時候, 客戶端套接字將產生 AE_WRITABLE 事件, 觸發命令回復處理器執行, 當命令回復處理器將命令回復全部寫入到套接字之后, 服務器就會解除客戶端套接字的 AE_WRITABLE 事件與命令回復處理器之間的關聯。

圖 IMAGE_COMMAND_PROGRESS 總結了上面描述的整個通訊過程, 以及通訊時用到的事件處理器。

digraph {    label = "\n圖 IMAGE_COMMAND_PROGRESS    客戶端和服務器的通訊過程";    splines = ortho;    rankdir = LR;    node [shape = box, height = 3.0];    client [label = "客\n戶\n端"];    server [label = "服\n務\n器"];    client -> server [label = "客戶端向服務器發送連接請求\n服務器執行連接應答處理器"];    client -> server [label = "\n\n客戶端向服務器發送命令請求\n服務器執行命令請求處理器"];    server -> client [label = "\n\n服務器向客戶端發送命令回復\n服務器執行命令回復處理器"];}

 

為啥 Redis 單線程模型也能效率這么高?

  • 純內存操作。
  • 核心是基於非阻塞的 IO 多路復用機制。
  • C 語言實現,一般來說,C 語言實現的程序“距離”操作系統更近,執行速度相對會更快。
  • 單線程反而避免了多線程的頻繁上下文切換問題,預防了多線程可能產生的競爭問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM