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