前言
Redis 是一個(gè)事件驅(qū)動(dòng)的內(nèi)存數(shù)據(jù)庫(kù),服務(wù)器需要處理兩種類型的事件。
下面就會(huì)介紹這兩種事件的實(shí)現(xiàn)原理。
文件事件
Redis 服務(wù)器通過(guò) socket 實(shí)現(xiàn)與客戶端(或其他redis/224812.html">redis服務(wù)器)的交互,文件事件就是服務(wù)器對(duì) socket 操作的抽象。 Redis 服務(wù)器,通過(guò)監(jiān)聽這些 socket 產(chǎn)生的文件事件并處理這些事件,實(shí)現(xiàn)對(duì)客戶端調(diào)用的響應(yīng)。
Reactor
Redis 基于 Reactor 模式開發(fā)了自己的事件處理器。
這里就先展開講一講 Reactor 模式。看下圖:
“I/O 多路復(fù)用模塊”會(huì)監(jiān)聽多個(gè) FD ,當(dāng)這些FD產(chǎn)生,accept,read,write 或 close 的文件事件。會(huì)向“文件事件分發(fā)器(dispatcher)”傳送事件。
文件事件分發(fā)器(dispatcher)在收到事件之后,會(huì)根據(jù)事件的類型將事件分發(fā)給對(duì)應(yīng)的 handler。
我們順著圖,從上到下的逐一講解 Redis 是怎么實(shí)現(xiàn)這個(gè) Reactor 模型的。
I/O 多路復(fù)用模塊
Redis 的 I/O 多路復(fù)用模塊,其實(shí)是封裝了操作系統(tǒng)提供的 select,epoll,avport 和 kqueue 這些基礎(chǔ)函數(shù)。向上層提供了一個(gè)統(tǒng)一的接口,屏蔽了底層實(shí)現(xiàn)的細(xì)節(jié)。
一般而言 Redis 都是部署到 Linux 系統(tǒng)上,所以我們就看看使用 Redis 是怎么利用 linux 提供的 epoll 實(shí)現(xiàn)I/O 多路復(fù)用。
首先看看 epoll 提供的三個(gè)方法:
/* * 創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大 */int epoll_create(int size);/* * 可以理解為,增刪改 fd 需要監(jiān)聽的事件 * epfd 是 epoll_create() 創(chuàng)建的句柄。 * op 表示 增刪改 * epoll_event 表示需要監(jiān)聽的事件,Redis 只用到了可讀,可寫,錯(cuò)誤,掛斷 四個(gè)狀態(tài) */int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);/* * 可以理解為查詢符合條件的事件 * epfd 是 epoll_create() 創(chuàng)建的句柄。 * epoll_event 用來(lái)存放從內(nèi)核得到事件的集合 * maxevents 獲取的最大事件數(shù) * timeout 等待超時(shí)時(shí)間 */int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
再看 Redis 對(duì)文件事件,封裝epoll向上提供的接口:
/* * 事件狀態(tài) */typedef struct aeApiState { // epoll_event 實(shí)例描述符 int epfd; // 事件槽 struct epoll_event *events;} aeApiState;/* * 創(chuàng)建一個(gè)新的 epoll */static int aeApiCreate(aeEventLoop *eventLoop)/* * 調(diào)整事件槽的大小 */static int aeApiResize(aeEventLoop *eventLoop, int setsize)/* * 釋放 epoll 實(shí)例和事件槽 */static void aeApiFree(aeEventLoop *eventLoop)/* * 關(guān)聯(lián)給定事件到 fd */static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)/* * 從 fd 中刪除給定事件 */static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask)/* * 獲取可執(zhí)行事件 */static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
所以看看這個(gè)ae_peoll.c 如何對(duì) epoll 進(jìn)行封裝的:
這樣 Redis 的利用 epoll 實(shí)現(xiàn)的 I/O 復(fù)用器就比較清晰了。
再往上一層次我們需要看看 ea.c 是怎么封裝的?
首先需要關(guān)注的是事件處理器的數(shù)據(jù)結(jié)構(gòu):
typedef struct aeFileEvent { // 監(jiān)聽事件類型掩碼, // 值可以是 AE_READABLE 或 AE_WRITABLE , // 或者 AE_READABLE | AE_WRITABLE int mask; /* one of AE_(READABLE|WRITABLE) */ // 讀事件處理器 aeFileProc *rfileProc; // 寫事件處理器 aeFileProc *wfileProc; // 多路復(fù)用庫(kù)的私有數(shù)據(jù) void *clientData;} aeFileEvent;
mask 就是可以理解為事件的類型。
除了使用 ae_peoll.c 提供的方法外,ae.c 還增加 “增刪查” 的幾個(gè) API。
事件分發(fā)器(dispatcher)
Redis 的事件分發(fā)器 ae.c/aeProcessEvents 不但處理文件事件還處理時(shí)間事件,所以這里只貼與文件分發(fā)相關(guān)的出部分代碼,dispather 根據(jù) mask 調(diào)用不同的事件處理器。
//從 epoll 中獲關(guān)注的事件numevents = aeApiPoll(eventLoop, tvp);for (j = 0; j < numevents; j++) { // 從已就緒數(shù)組中獲取事件 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 確保讀/寫事件只能執(zhí)行其中一個(gè) 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++;}
可以看到這個(gè)分發(fā)器,根據(jù) mask 的不同將事件分別分發(fā)給了讀事件和寫事件。
文件事件處理器的類型
Redis 有大量的事件處理器類型,我們就講解處理一個(gè)簡(jiǎn)單命令涉及到的三個(gè)處理器:
文件事件實(shí)現(xiàn)總結(jié)
我們按照開始給出的 Reactor 模型,從上到下講解了文件事件處理器的實(shí)現(xiàn),下面將會(huì)介紹時(shí)間時(shí)間的實(shí)現(xiàn)。
時(shí)間事件
Reids 有很多操作需要在給定的時(shí)間點(diǎn)進(jìn)行處理,時(shí)間事件就是對(duì)這類定時(shí)任務(wù)的抽象。
先看時(shí)間事件的數(shù)據(jù)結(jié)構(gòu):
/* Time event structure * * 時(shí)間事件結(jié)構(gòu) */typedef struct aeTimeEvent { // 時(shí)間事件的唯一標(biāo)識(shí)符 long long id; /* time event identifier. */ // 事件的到達(dá)時(shí)間 long when_sec; /* seconds */ long when_ms; /* milliseconds */ // 事件處理函數(shù) aeTimeProc *timeProc; // 事件釋放函數(shù) aeEventFinalizerProc *finalizerProc; // 多路復(fù)用庫(kù)的私有數(shù)據(jù) void *clientData; // 指向下個(gè)時(shí)間事件結(jié)構(gòu),形成鏈表 struct aeTimeEvent *next;} aeTimeEvent;
看見 next 我們就知道這個(gè) aeTimeEvent 是一個(gè)鏈表結(jié)構(gòu)。看圖:
注意:這是一個(gè)按照id倒序排列的鏈表,并沒有按照事件順序排序。
processTimeEvent
Redis 使用這個(gè)函數(shù)處理所有的時(shí)間事件,我們整理一下執(zhí)行思路:
綜合調(diào)度器(aeProcessEvents)
綜合調(diào)度器是 Redis 統(tǒng)一處理所有事件的地方。我們梳理一下這個(gè)函數(shù)的簡(jiǎn)單邏輯:
// 1. 獲取離當(dāng)前時(shí)間最近的時(shí)間事件shortest = aeSearchNearestTimer(eventLoop);// 2. 獲取間隔時(shí)間timeval = shortest - nowTime;// 如果timeval 小于 0,說(shuō)明已經(jīng)有需要執(zhí)行的時(shí)間事件了。if(timeval < 0){ timeval = 0}// 3. 在 timeval 時(shí)間內(nèi),取出文件事件。numevents = aeApiPoll(eventLoop, timeval);// 4.根據(jù)文件事件的類型指定不同的文件處理器if (AE_READABLE) { // 讀事件 rfileProc(eventLoop,fd,fe->clientData,mask);} // 寫事件if (AE_WRITABLE) { wfileProc(eventLoop,fd,fe->clientData,mask);}
以上的偽代碼就是整個(gè) Redis 事件處理器的邏輯。
我們可以再看看誰(shuí)執(zhí)行了這個(gè) aeProcessEvents:
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { // 如果有需要在事件處理前執(zhí)行的函數(shù),那么運(yùn)行它 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); // 開始處理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS); }}
然后我們?cè)倏纯词钦l(shuí)調(diào)用了 eaMain:
int main(int argc, char **argv) { //一些配置和準(zhǔn)備 ... aeMain(server.el); //結(jié)束后的回收工作 ...}
我們?cè)?Redis 的 main 方法中找個(gè)了它。
這個(gè)時(shí)候我們整理出的思路就是:
所以我們說(shuō) Redis 是一個(gè)事件驅(qū)動(dòng)的程序,期間我們發(fā)現(xiàn),Redis 沒有 fork 過(guò)任何線程。所以也可以說(shuō) Redis 是一個(gè)基于事件驅(qū)動(dòng)的單線程應(yīng)用。
總結(jié)
在后端的面試中 Redis 總是一個(gè)或多或少會(huì)問(wèn)到的問(wèn)題。
讀完這篇文章你也許就能回答這幾個(gè)問(wèn)題:
為什么 Redis 是一個(gè)單線程應(yīng)用?
為什么 Redis 是一個(gè)單線程應(yīng)用,卻有如此高的性能?
如果你用本文提供的知識(shí)點(diǎn)回答這兩個(gè)問(wèn)題,一定會(huì)在面試官心中留下一個(gè)高大的形象。
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)VEVB武林網(wǎng)的支持。
|
新聞熱點(diǎn)
疑難解答
圖片精選