日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

本文介紹了 redis 核心原理和架構:基于事件驅動的模型。事件模型是構成 Redis 內核的引擎,Redis 的豐富功能和組件都是構建在這個模型上的。如果你使用過 Redis,那么本文可以為你打開一道進入 Redis 內部世界的門,窺探 Redis 如何構建它的帝國。

本文先對 Redis 使用的事件模型和原理進行介紹,然后按以下主題順序展開:

  1. Redis 主程序啟動流程
  2. 事件循環(eventloop)
  3. 事件處理器 (event handler)
  4. 事件處理流程

最后以一次客戶端 SET 命令操作為例子,講解一個請求在 Redis 內部的流轉是如何完成的。

閱讀之前

為了方便公眾號上進行閱讀,幫助讀者快速掌握 Redis 核心原理,本文對 Redis 模型進行了簡化,去掉了大量的檢查和異常處理流程,并且僅在必要的時候通過代碼說明。

本文參考的源碼基于編寫時的最新分支 Redis 5.0.3,實際對照中發現 Redis 的核心邏輯在歷史版本迭代中變化不大,也體現了 Redis 的這個核心邏輯的地位。

一、Redis 事件驅動模型

1.1 事件驅動模型

事件驅動,顧名思義,只有在發生某些事件的時候,程序才會有所行動。

事件驅動模型在架構設計領域也稱為 Reactor 模式,體現的是一種被動響應的特征。

事件驅動模型通常可以抽象為如下圖所示流程:

怎么樣用Redis構建自己的代碼帝國?(Redis的架構和核心原理)

 

主程序處于一個阻塞狀態的事件循環(event loop)中等待事件(event),當有事件發生時,根據事件的屬性分發到相應的處理函數進行處理。事件以并發的方式發送到服務處理器 (service handler),服務處理器將事件整合到一個有序隊列中(這過程稱為 demultiplexes),并分發到具體的請求處理器 (request handler)進行處理。

為了閱讀的方便,因為「事件」這個詞在中文中較常見,所以下文針對事件模型中的「事件」等專用術語,會進行特定的標識,如:事件循環 (event loop),事件 (event),處理器 (handler)等。

1.2 Redis 核心原理

Redis 在事件驅動模型下工作,當有來自外部或內部的請求的時候,才會執行相關的流程。

Redis 程序的整個運作都是圍繞事件循環 (event loop)進行的。

事件循環對于 Redis 而言,就像是一臺車的引擎一樣,提供了整個系統所需的流轉動力。所有其他的組件都是基于這個引擎的基礎上組合和構建起來的。可以說理解了 Redis 的事件循環就能了解 Redis 的工作原理的核心。

Redis 事件模型如下圖所示:

 

怎么樣用Redis構建自己的代碼帝國?(Redis的架構和核心原理)

 

 

事件循環 eventloop同時監控多個事件,這里的事件本質上是 Redis 對于連接套接字的抽象。

當套接字變為可讀或者可寫狀態時,就會觸發該事件,把就緒的事件放在一個待處理事件的隊列中,以有序 (sequentially)、同步 (synchronously) 的方式發送給事件處理器進行處理。這個過程在 Redis 中被稱為Fire。

Redis 的事件循環會保存兩個列表:events和fired列表,前者表示正在監聽的事件,后者表示就緒事件,可以被進一步執行。

在具體實現時,Redis 采用 IO 多路復用 (multiplexing) 的方式,封裝了操作系統底層 select/epoll 等函數,實現對多個套接字 (socket) 的監聽,這些套接字就是對應多個不同客戶端的連接。

最后由對應的處理器將處理的結果返回給客戶端去。

Redis事件的來源有兩種:文件事件和時間事件,限于篇幅問題,本文主要介紹文件事件的處理流程,時間事件會在文章最后做簡要的說明。

以上就概括了Redis 處理用戶請求的大致過程。從這個過程我們可以發現:

  1. Redis 處理所有命令都是順序執行的,其中包括來自客戶端的連接請求。所以當 Redis 在處理一個復雜度高、時間很長的請求(比如 KEYS 命令)的時候,其他客戶端的連接都沒辦法相應。
  2. Redis 內部定時執行的任務也是放在順序隊列中處理,其中也可能包含時間較長的任務,比如自動刪除一個過期的大 Key,比如很大 list, hash, set 等。所以有時候會遇到明明業務沒有主動操作復雜,但也會出現卡頓的問題。

1.3 事件驅動模型的優勢

有利于架構解耦和模塊化開發

有利于功能架構實現上更加解耦,模塊的可重用性更高。因事件循環的流程本身和具體的處理邏輯之間是獨立的,只要在創建事件的時候關聯特定的處理邏輯(事件處理器),就可以完成一次事件的創建和處理。

有利于減小高并發量情況下對性能的影響

根據論文 SEDA: An Architecture for Well-Conditioned, Scalable Internet Services 的測試結果顯示,相比一個連接分配一個線程的模型, Reactor 模式(固定線程數)在連接數增大的情況下吞吐量不會明顯降低,延時也不會也受到顯著的影響。

二、事件循環的 Redis 實現

下面開始,會對 Redis 如何實現事件循環進行說明,會涉及到一些源碼的實現部分,如果不感興趣可以直接跳到第三節看 Redis 怎么利用事件處理模型來處理具體的命令。

2.1 Redis 事件循環 Event Loop

Redis 的事件循環,最直觀的理解,就是一個在不斷等待事件的一個無限循環,直到 Redis 程序退出。

Redis 實現事件循環主要涉及三個源碼文件:server.c, ae.c, networking.c。

  • server.c 的 main()函數是整個 Redis 程序的開始,我們也從這里開始觀察 Redis 的行為。
  • ae.c實現事件循環和事件的相關功能。
  • networking.c則負責處理網絡IO相關的功能。

 

怎么樣用Redis構建自己的代碼帝國?(Redis的架構和核心原理)

 

 

a. 初始化 Redis 配置

初始化的過程主要做三個事情:

  1. 加載配置
  2. 創建事件循環
  3. 執行事件循環

簡化后的代碼如下:(跳過不影響理解)

// 0. 定義服務器主要結構體, 加載服務器配置
struct redisServer server;
initServerConfig();
loadServerConfig();
// 1. 根據配置參數初始化,
initServer() 
{
 // 1.1 實際創建事件循環
 server.el = aeCreateEventLoop();
 // 1.2 為事件循環注冊一個可讀事件,用于響應外部客戶端請求
 aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)
}
// 2. 執行事件循環,等待連接和命令請求
aeMain(server.el);

初始化過程中被創建的server.el包含了兩個事件的列表,它的結構體實現如下:

typedef struct aeEventLoop
{
 aeFileEvent events[AE_SETSIZE]; /* 注冊的事件,被 eventloop 監聽 */
 aeFiredEvent fired[AE_SETSIZE]; /* 有讀寫操作需要執行的事件(就緒事件) */
} aeEventLoop;

b. 創建事件循環

主循環體aeMain()在ae.c文件中被實現,簡化后的代碼如下:

void aeMain(aeEventLoop *eventLoop) {
 while (!eventLoop->stop) {
 aeProcessEvents(eventLoop, AE_ALL_EVENTS);
 }
}

事件循環主要就是一個while循環,不斷去輪詢是否有就緒的事件需要處理,具體的處理函數是aeProcessEvents,接下來會有對這個函數有更詳細的介紹。

c. 創建用于監聽端口的事件

在上述 Redis 在初始化時,程序會創建一個關聯了acceptTcpHandler處理器的可讀事件:

aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)

這個可讀事件注冊到事件循環中,就實現了 Redis 對外提供的服務地址和端口的連接服務。具體的內容下一個小節事件處理器中介紹。

2.2 事件處理器 Event Handler

所有事件被創建時,都會關聯一個處理器 (handler),并注冊到事件循環中,事件處理器用于具體的讀寫操作。

Redis 的常用幾個事件處理器有:

  • 響應連接的處理器acceptTcpHandler()
  • 讀取客戶端命令的處理器readQueryFromClient()
  • 返回處理結果的處理器sendReplyToClient()

以上處理器均在networking.c文件下實現,該文件負責 Redis 所有網絡 IO 功能的實現。

一個客戶端一次正常的連接和命令操作流程,可以通過上述三個處理器完成。

怎么樣用Redis構建自己的代碼帝國?(Redis的架構和核心原理)

 

當 Redis 需要監聽某個套接字的時候,就會創建一個事件,并注冊到事件循環中進行監聽,Redis 將處理器以參數的方式關聯到事件中。

比如以下是注冊一個可讀事件的操作:

aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c)
  • server.el:事件循環 eventloop,一個服務器只有一個el
  • fd:表示這個客戶端連接的文件描述符,每個客戶端連接對應一個
  • AE_READABLE:表示這是一個可讀事件,可以理解為客戶端準備進行寫操作
  • readQueryFromClient: 這個事件關聯的處理器,當事件就緒后,就會調用此處理器
  • c:表示這個客戶端在Redis中指向的變量

注冊完畢后,事件循環就會將這個事件(套接字)加入到監聽的范圍,當事件可讀時,Redis 就會將這個事件發送到待處理事件隊列中等待處理,等到可讀就緒時,會被readQueryFromClient處理器處理。

可以看到整個過程中事件循環和不同處理器之間是解耦的,互不干擾。這樣實現提高了代碼的簡潔和重用。

2.3 事件處理 Process Events

在 Redis 完成初始化、創建事件循環后,就會處于等待和處理事件的狀態:無限循環aeProcessEvents()函數。

這個函數在ae.c中實現,該文件主要負責事件循環的實現,在aeProcessEvents()中具體做了幾個事情:

  1. 調用IO多路復用函數(select, epoll, evport, kqueue中的其中一種),阻塞等待事件變成就緒狀態或者直到超時,如果有事件就緒,就會將相應事件加入到eventLoop的待處理事件隊列 eventLoop->fired 中,然后進入下一個循環。
 numevents = aeApiPoll(eventLoop, tvp); 
  1. 如果在上一步中,發現有numevents個事件被觸發,就會將就緒隊列的事件一個個按順序進行處理,處理的函數為
 for (j = 0; j < numevents; j++) {
 aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
 fe->rfileProc() // 讀事件處理
 fe->wfileProc() // 寫事件處理
 }

fe就是要處理的文件事件 file event,對應讀操作或寫操作。至于處理的具體操作,則由創建事件時自身關聯的處理器決定的,事件循環不需要關注。

  1. 最后一步:如果有時間事件,則進行時間事件的處理:
 processTimeEvents(eventLoop);

至此,Redis 的事件循環的機制已經介紹完畢,可以觀察到整個事件循環的邏輯過程都沒有涉及具體的命令操作,只需要定義事件的類型和處理器即可。可以說這部分就是Reactor 模式體現出來的一個好處:接收事件和處理流程的實現相互解耦。

三、一次命令操作的完整流程

本章是建立在 Redis 已經完成了初始化工作,主要是創建事件循環之后,Redis 接受一個客戶端操作的完整流程的介紹。如果對初始化過程還有問題,請參考上文。

本章主要分為兩個階段:

  1. 第一個階段:一個外部客戶端與 Redis 服務器建立 TCP 連接。
  2. 比如我們常用的 Telnet 到 Redis 端口的操作。
 ? ~ telnet 127.0.0.1 6379
 Trying 127.0.0.1...
 Connected to 127.0.0.1.
 Escape character is '^]'.
  1. 第二階段:已經建立連接的客戶端,對Redis 發起一次SET命令的操作。
 set a 1
 +OK

3.1 一個客戶端連接進服務器的過程

如圖,展示一個新的外部客戶端與 Redis 服務器建立連接的過程。

怎么樣用Redis構建自己的代碼帝國?(Redis的架構和核心原理)

 

當有客戶端連接到 Redis 服務器的時候,注冊在事件循環中的監聽服務端口的事件就會變成讀就緒狀態,從而觸發這個事件到待處理事件隊列中,準備調用acceptTcpHandler進行處理。

  1. 為在服務器端創建一個對應本次連接的套接字。
  2. 把服務端套接字的文件描述符cfd作為參數,創建client變量。
  3. 為該客戶端連接創建并注冊一個關聯了readQueryFromClient處理器的可讀事件到事件循環,用于下一步接收并執行命令的工作。

3.2 一次客戶端連接和調用命令的執行流程

如圖展示一個客戶端已經完成了連接,對 Redis 服務器發起一次SET操作后,Redis 處理命令的完整流程。

怎么樣用Redis構建自己的代碼帝國?(Redis的架構和核心原理)

 

在上一節中提到,當一個客戶端建立連接后,會有一個可讀事件關聯到事件循環,等待接收命令。當有客戶端發起一次命令操作后,Redis 就會調用readQueryFromClient處理器,對用戶發送過來的請求,按 RESP (REdis Serialization Protocol) 進行解析處理后,調用相關的命令進行處理。

  1. 調用命令的函數主要做兩個事情:(1)查找對應的命令,比如這里的SET(2)調用該命令關聯的函數進行處理,這里就是setCommand。
  2. setCommand函數將客戶端傳進來的參數,變更數據庫對應 KEY 的值,然后回復客戶端。
  3. 回復客戶端addReply函數將返回給客戶端的內容,寫到客戶端變量的輸出緩沖client.buf中,等待發送給客戶端。

返回結果給客戶端

以上是整個SET命令的事件處理,不過在這個時候,返回給用戶的回復內容,只存放于服務器的客戶端變量輸出緩沖中。至于將結果返回給用戶的過程,取決于版本,有不同的操作。

在 4.0 以前,每次的addReply操作會創建一個寫事件,然后放到事件循環中執行。

而 4.0 開始,在每次重新進入一個新的循環之前,就是eventLoop->beforesleep();這個操作,Redis 會嘗試直接發送給客戶端,只有當發送的內容超過一定大小,無法一次發送完成的時候,才會去創建一個可寫事件。

有興趣的讀者可以去看下 Redis 作者的這個 commit:

antirez in commit 1c7d87d:
 Avoid installing the client write handler when possible.

目的是減少一次系統調用,適用于大部分操作類命令的回復。

可以觀察到,整個操作的實現過程,和事件循環本身沒有交集的(沒有涉及到ae.c),開發者只需要關心具體命令的處理邏輯即可。

四、補充說明

  • 事件都是來源于外部客戶端嗎?
  • 這要看怎么定義“外部客戶端”了。首先事件本身分為兩種大類:文件事件和時間事件。本文主要介紹文件事件。而文件事件的產生可以是來源于網絡客戶端的連接,正如本文所描述的,也可以來自 Redis 集群內部運行需要,會使用一些偽客戶端來觸發一些文件事件。
  • 舉個例子,當有從節點 (slave/replica) 向主節點 (master) 發起一次同步的時候,在 Redis 就會產生一個需要處理同步數據的事件。不過嚴格意義上來講,這個從節點對于主節點 Redis 來說,也屬于“外部客戶端”。正常情況下,Redis 自身不會主動產生文件事件。
  • Redis 是怎么定期更新狀態、刪除過期KEY的?
  • 讀者大概猜到我要引出時間事件這個概念了。Redis 會定期執行服務器的檢查,以及一些周期操作,這個周期由參數hz決定,默認情況下是100毫秒觸發一次檢查,執行該周期內的時間事件。
  • 時間事件 是 Redis 也是核心流程中重要的一個組成部分,限于篇幅不在這里詳細介紹。但有了對事件循環的認識,要理解時間事件本身也不會太困難。

分享到:
標簽:Redis
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定