您好,登錄后才能下訂單哦!
這篇文章給大家介紹Redis中事件驅動模型的作用是什么,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
前言
Redis 是一個事件驅動的內存數據庫,服務器需要處理兩種類型的事件。
文件事件
時間事件
下面就會介紹這兩種事件的實現原理。
文件事件
Redis 服務器通過 socket 實現與客戶端(或其他redis服務器)的交互,文件事件就是服務器對 socket 操作的抽象。 Redis 服務器,通過監聽這些 socket 產生的文件事件并處理這些事件,實現對客戶端調用的響應。
Reactor
Redis 基于 Reactor 模式開發了自己的事件處理器。
這里就先展開講一講 Reactor 模式。看下圖:
“I/O 多路復用模塊”會監聽多個 FD ,當這些FD產生,accept,read,write 或 close 的文件事件。會向“文件事件分發器(dispatcher)”傳送事件。
文件事件分發器(dispatcher)在收到事件之后,會根據事件的類型將事件分發給對應的 handler。
我們順著圖,從上到下的逐一講解 Redis 是怎么實現這個 Reactor 模型的。
I/O 多路復用模塊
Redis 的 I/O 多路復用模塊,其實是封裝了操作系統提供的 select,epoll,avport 和 kqueue 這些基礎函數。向上層提供了一個統一的接口,屏蔽了底層實現的細節。
一般而言 Redis 都是部署到 Linux 系統上,所以我們就看看使用 Redis 是怎么利用 linux 提供的 epoll 實現I/O 多路復用。
首先看看 epoll 提供的三個方法:
/* * 創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大 */ int epoll_create(int size); /* * 可以理解為,增刪改 fd 需要監聽的事件 * epfd 是 epoll_create() 創建的句柄。 * op 表示 增刪改 * epoll_event 表示需要監聽的事件,Redis 只用到了可讀,可寫,錯誤,掛斷 四個狀態 */ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /* * 可以理解為查詢符合條件的事件 * epfd 是 epoll_create() 創建的句柄。 * epoll_event 用來存放從內核得到事件的集合 * maxevents 獲取的最大事件數 * timeout 等待超時時間 */ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
再看 Redis 對文件事件,封裝epoll向上提供的接口:
/* * 事件狀態 */ typedef struct aeApiState { // epoll_event 實例描述符 int epfd; // 事件槽 struct epoll_event *events; } aeApiState; /* * 創建一個新的 epoll */ static int aeApiCreate(aeEventLoop *eventLoop) /* * 調整事件槽的大小 */ static int aeApiResize(aeEventLoop *eventLoop, int setsize) /* * 釋放 epoll 實例和事件槽 */ static void aeApiFree(aeEventLoop *eventLoop) /* * 關聯給定事件到 fd */ static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) /* * 從 fd 中刪除給定事件 */ static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) /* * 獲取可執行事件 */ static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
所以看看這個ae_peoll.c 如何對 epoll 進行封裝的:
aeApiCreate() 是對 epoll.epoll_create() 的封裝。
aeApiAddEvent()和aeApiDelEvent() 是對 epoll.epoll_ctl()的封裝。
aeApiPoll() 是對 epoll_wait()的封裝。
這樣 Redis 的利用 epoll 實現的 I/O 復用器就比較清晰了。
再往上一層次我們需要看看 ea.c 是怎么封裝的?
首先需要關注的是事件處理器的數據結構:
typedef struct aeFileEvent { // 監聽事件類型掩碼, // 值可以是 AE_READABLE 或 AE_WRITABLE , // 或者 AE_READABLE | AE_WRITABLE int mask; /* one of AE_(READABLE|WRITABLE) */ // 讀事件處理器 aeFileProc *rfileProc; // 寫事件處理器 aeFileProc *wfileProc; // 多路復用庫的私有數據 void *clientData; } aeFileEvent;
mask 就是可以理解為事件的類型。
除了使用 ae_peoll.c 提供的方法外,ae.c 還增加 “增刪查” 的幾個 API。
增:aeCreateFileEvent
刪:aeDeleteFileEvent
查: 查包括兩個維度 aeGetFileEvents 獲取某個 fd 的監聽類型和aeWait等待某個fd 直到超時或者達到某個狀態。
事件分發器(dispatcher)
Redis 的事件分發器 ae.c/aeProcessEvents 不但處理文件事件還處理時間事件,所以這里只貼與文件分發相關的出部分代碼,dispather 根據 mask 調用不同的事件處理器。
//從 epoll 中獲關注的事件 numevents = aeApiPoll(eventLoop, tvp); for (j = 0; j < numevents; j++) { // 從已就緒數組中獲取事件 aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; int mask = eventLoop->fired[j].mask; int fd = eventLoop->fired[j].fd; int rfired = 0; // 讀事件 if (fe->mask & mask & AE_READABLE) { // rfired 確保讀/寫事件只能執行其中一個 rfired = 1; fe->rfileProc(eventLoop,fd,fe->clientData,mask); } // 寫事件 if (fe->mask & mask & AE_WRITABLE) { if (!rfired || fe->wfileProc != fe->rfileProc) fe->wfileProc(eventLoop,fd,fe->clientData,mask); } processed++; }
可以看到這個分發器,根據 mask 的不同將事件分別分發給了讀事件和寫事件。
文件事件處理器的類型
Redis 有大量的事件處理器類型,我們就講解處理一個簡單命令涉及到的三個處理器:
acceptTcpHandler 連接應答處理器,負責處理連接相關的事件,當有client 連接到Redis的時候們就會產生 AE_READABLE 事件。引發它執行。
readQueryFromClinet 命令請求處理器,負責讀取通過 sokect 發送來的命令。
sendReplyToClient 命令回復處理器,當Redis處理完命令,就會產生 AE_WRITEABLE 事件,將數據回復給 client。
文件事件實現總結
我們按照開始給出的 Reactor 模型,從上到下講解了文件事件處理器的實現,下面將會介紹時間時間的實現。
時間事件
Reids 有很多操作需要在給定的時間點進行處理,時間事件就是對這類定時任務的抽象。
先看時間事件的數據結構:
/* Time event structure * * 時間事件結構 */ typedef struct aeTimeEvent { // 時間事件的唯一標識符 long long id; /* time event identifier. */ // 事件的到達時間 long when_sec; /* seconds */ long when_ms; /* milliseconds */ // 事件處理函數 aeTimeProc *timeProc; // 事件釋放函數 aeEventFinalizerProc *finalizerProc; // 多路復用庫的私有數據 void *clientData; // 指向下個時間事件結構,形成鏈表 struct aeTimeEvent *next; } aeTimeEvent;
看見 next 我們就知道這個 aeTimeEvent 是一個鏈表結構。看圖:
注意:這是一個按照id倒序排列的鏈表,并沒有按照事件順序排序。
processTimeEvent
Redis 使用這個函數處理所有的時間事件,我們整理一下執行思路:
記錄最新一次執行這個函數的時間,用于處理系統時間被修改產生的問題。
遍歷鏈表找出所有 when_sec 和 when_ms 小于現在時間的事件。
執行事件對應的處理函數。
檢查事件類型,如果是周期事件則刷新該事件下一次的執行事件。
否則從列表中刪除事件。
綜合調度器(aeProcessEvents)
綜合調度器是 Redis 統一處理所有事件的地方。我們梳理一下這個函數的簡單邏輯:
// 1. 獲取離當前時間最近的時間事件 shortest = aeSearchNearestTimer(eventLoop); // 2. 獲取間隔時間 timeval = shortest - nowTime; // 如果timeval 小于 0,說明已經有需要執行的時間事件了。 if(timeval < 0){ timeval = 0 } // 3. 在 timeval 時間內,取出文件事件。 numevents = aeApiPoll(eventLoop, timeval); // 4.根據文件事件的類型指定不同的文件處理器 if (AE_READABLE) { // 讀事件 rfileProc(eventLoop,fd,fe->clientData,mask); } // 寫事件 if (AE_WRITABLE) { wfileProc(eventLoop,fd,fe->clientData,mask); }
以上的偽代碼就是整個 Redis 事件處理器的邏輯。
我們可以再看看誰執行了這個 aeProcessEvents:
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { // 如果有需要在事件處理前執行的函數,那么運行它 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); // 開始處理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
然后我們再看看是誰調用了 eaMain:
int main(int argc, char **argv) { //一些配置和準備 ... aeMain(server.el); //結束后的回收工作 ... }
我們在 Redis 的 main 方法中找個了它。
這個時候我們整理出的思路就是:
Redis 的 main() 方法執行了一些配置和準備以后就調用 eaMain() 方法。
eaMain() while(true) 的調用 aeProcessEvents()。
所以我們說 Redis 是一個事件驅動的程序,期間我們發現,Redis 沒有 fork 過任何線程。所以也可以說 Redis 是一個基于事件驅動的單線程應用。
關于Redis中事件驅動模型的作用是什么就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。