淺談Redis的IO多路復(fù)用
面試回答
創(chuàng)建epoll實(shí)例,每一個(gè)客戶端(socket)都對(duì)應(yīng)一個(gè)FD(文件描述符),將這些FD注冊(cè)到epoll中,并為FD綁定回調(diào)函數(shù),主線程通過epoll_await()獲取等待就緒事件,epoll返回就緒的FD和事件,內(nèi)核將就緒的 FD 拷貝到用戶態(tài)。redis主線程逐一處理(其實(shí)就是一個(gè)死循環(huán))。
可以講講select、poll、epoll
為什么redis 6.0 之后網(wǎng)絡(luò)IO的處理改成多線程的方式了
- 第一個(gè)缺點(diǎn):因?yàn)橹挥幸粋€(gè)進(jìn)程,無法充分利用 多核 CPU 的性能;
- 第二個(gè)缺點(diǎn):handler對(duì)象在業(yè)務(wù)處理時(shí),整個(gè)進(jìn)程是無法處理其他連接的事件的,如果業(yè)務(wù)處理耗時(shí)比較長(zhǎng),那么就造成響應(yīng)的延遲;
什么是handler在單 Reactor 單線程模型中,Handler(事件處理器)在處理某個(gè)連接的業(yè)務(wù)邏輯時(shí),會(huì)獨(dú)占整個(gè)線程。 因?yàn)?Redis 6.0 之前的網(wǎng)絡(luò)模型是單線程的,所以當(dāng) Handler 正在處理一個(gè)耗時(shí)操作時(shí),其他連接的事件(讀、寫、accept)都無法被處理,從而導(dǎo)致整體響應(yīng)變慢。
Redis為了跨平臺(tái)兼容,將不同系統(tǒng)的多路復(fù)用接口(epoll/kqueue/select等)封裝成了統(tǒng)一的aeEventLoop(事件循環(huán))框架,核心源碼集中在ae.c/ae.h和對(duì)應(yīng)系統(tǒng)的實(shí)現(xiàn)文件(如ae_epoll.c/ae_kqueue.c)中。
下面我會(huì)從核心數(shù)據(jù)結(jié)構(gòu)、初始化流程、事件注冊(cè)、事件等待/處理四個(gè)維度,結(jié)合關(guān)鍵源碼片段拆解Redis IO多路復(fù)用的實(shí)現(xiàn)。
一、先明確核心源碼文件
Redis的IO多路復(fù)用核心代碼在以下文件(Redis 6.x/7.x版本):
- src/ae.h:定義事件循環(huán)、事件對(duì)象的核心數(shù)據(jù)結(jié)構(gòu);
- src/ae.c:事件循環(huán)的通用邏輯(跨平臺(tái));
- src/ae_epoll.c:Linux下epoll的具體實(shí)現(xiàn);
- src/ae_kqueue.c:macOS/FreeBSD下kqueue的實(shí)現(xiàn);
- src/ae_select.c:兼容select/poll的實(shí)現(xiàn)。
二、核心數(shù)據(jù)結(jié)構(gòu)(ae.h)
Redis先定義了統(tǒng)一的抽象層,屏蔽不同多路復(fù)用器的差異,核心結(jié)構(gòu)如下:
1. 事件對(duì)象(aeFileEvent)
表示一個(gè)文件描述符(FD)對(duì)應(yīng)的監(jiān)聽事件(讀/寫):
// ae.h
typedef struct aeFileEvent {
// 事件類型:AE_READABLE(讀事件) / AE_WRITABLE(寫事件)
int mask;
// 讀事件回調(diào)函數(shù)(客戶端發(fā)命令時(shí)觸發(fā))
aeFileProc *rfileProc;
// 寫事件回調(diào)函數(shù)(給客戶端返回結(jié)果時(shí)觸發(fā))
aeFileProc *wfileProc;
// 私有數(shù)據(jù)(通常指向客戶端連接結(jié)構(gòu)client)
void *clientData;
} aeFileEvent;
2. 就緒事件(aeFiredEvent)
存儲(chǔ)epoll返回的“就緒FD+事件”,供后續(xù)處理:
// ae.h
typedef struct aeFiredEvent {
int fd; // 就緒的文件描述符
int mask; // 就緒的事件類型(AE_READABLE/AE_WRITABLE)
} aeFiredEvent;
3. 事件循環(huán)(aeEventLoop)
整個(gè)IO多路復(fù)用的核心上下文,管理所有監(jiān)聽事件和就緒事件:
// ae.h
typedef struct aeEventLoop {
int maxfd; // 當(dāng)前監(jiān)聽的最大FD
int setsize; // 最大支持的FD數(shù)量(可配置)
long long timeEventNextId; // 時(shí)間事件ID(定時(shí)任務(wù)用)
time_t lastTime; // 最后一次處理時(shí)間事件的時(shí)間
aeFileEvent *events; // 所有注冊(cè)的文件事件(數(shù)組,下標(biāo)=FD)
aeFiredEvent *fired; // 就緒的文件事件(數(shù)組)
aeTimeEvent *timeEventHead; // 時(shí)間事件鏈表(定時(shí)任務(wù))
int stop; // 事件循環(huán)停止標(biāo)志
void *apidata; // 多路復(fù)用器私有數(shù)據(jù)(如epoll的fd)
aeBeforeSleepProc *beforesleep; // 事件循環(huán)休眠前的回調(diào)
aeBeforeSleepProc *aftersleep; // 事件循環(huán)喚醒后的回調(diào)
} aeEventLoop;
三、核心流程(源碼級(jí)拆解)
以Linux下epoll實(shí)現(xiàn)為例,拆解Redis IO多路復(fù)用的完整流程:
1. 事件循環(huán)初始化(aeCreateEventLoop)
// ae.c
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop = zmalloc(sizeof(aeEventLoop));
eventLoop->setsize = setsize;
eventLoop->maxfd = -1;
// 初始化事件數(shù)組:下標(biāo)=FD,存儲(chǔ)該FD的監(jiān)聽事件
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
// 初始化就緒事件數(shù)組:存儲(chǔ)epoll返回的就緒事件
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
eventLoop->stop = 0;
eventLoop->lastTime = time(NULL);
// 初始化具體的多路復(fù)用器(epoll)
// aeApiCreate會(huì)調(diào)用epoll_create()創(chuàng)建epoll實(shí)例
if (aeApiCreate(eventLoop) == -1) {
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
return NULL;
}
return eventLoop;
}
// ae_epoll.c:epoll的初始化實(shí)現(xiàn)
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
// 創(chuàng)建epoll實(shí)例,返回epoll_fd
state->epfd = epoll_create(1024); // 1024是歷史參數(shù),現(xiàn)在無意義
eventLoop->apidata = state;
return (state->epfd == -1) ? -1 : 0;
}
核心邏輯:
- 創(chuàng)建事件循環(huán)對(duì)象,初始化事件數(shù)組(監(jiān)聽事件)和就緒事件數(shù)組;
- 調(diào)用
aeApiCreate創(chuàng)建epoll實(shí)例,返回的epoll_fd存儲(chǔ)在eventLoop->apidata中。
2. 注冊(cè)事件(aeCreateFileEvent)
當(dāng)Redis監(jiān)聽端口(6379)或新建客戶端連接時(shí),會(huì)注冊(cè)FD的監(jiān)聽事件:
// ae.c
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd];
// 調(diào)用epoll_ctl注冊(cè)事件到epoll實(shí)例
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
// 設(shè)置事件的回調(diào)函數(shù)和私有數(shù)據(jù)
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
// ae_epoll.c:epoll添加事件的實(shí)現(xiàn)
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; // epoll_event結(jié)構(gòu)體
// 獲取該FD已注冊(cè)的事件(避免重復(fù)注冊(cè))
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
// 轉(zhuǎn)換Redis事件掩碼為epoll事件掩碼
ee.events = 0;
mask |= eventLoop->events[fd].mask; // 合并已有事件
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
// 調(diào)用epoll_ctl注冊(cè)/修改事件
if (epoll_ctl(state->epfd, op, fd, &ee) == -1) return -1;
return 0;
}
核心邏輯:
- 將FD和監(jiān)聽事件(讀/寫)注冊(cè)到epoll實(shí)例(
epoll_ctl的EPOLL_CTL_ADD/EPOLL_CTL_MOD); - 為該FD綁定回調(diào)函數(shù)(比如讀事件回調(diào)
readQueryFromClient,處理客戶端命令)。
3. 事件循環(huán)(aeMain)
Redis啟動(dòng)后,主線程進(jìn)入無限循環(huán),等待并處理就緒事件:
// ae.c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
// 無限循環(huán),直到stop被置1(Redis關(guān)閉)
while (!eventLoop->stop) {
// 休眠前的回調(diào)(比如處理定時(shí)任務(wù)、統(tǒng)計(jì))
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 核心:等待就緒事件,返回就緒的事件數(shù)量
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
// ae.c:處理事件的核心函數(shù)
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
int processed = 0, numevents;
// 調(diào)用aeApiPoll(epoll_wait)等待就緒事件
numevents = aeApiPoll(eventLoop, tvp);
processed += numevents;
// 遍歷所有就緒事件,逐個(gè)處理
for (int 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 = 1;
// 調(diào)用讀事件回調(diào)(比如readQueryFromClient)
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
}
// 處理寫事件
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc) {
// 調(diào)用寫事件回調(diào)(比如sendReplyToClient)
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
}
processed++;
}
// 處理時(shí)間事件(定時(shí)任務(wù),如過期鍵清理)
...
return processed;
}
// ae_epoll.c:epoll_wait等待就緒事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval;
// 調(diào)用epoll_wait,阻塞等待就緒事件,結(jié)果存入eventLoop->fired
retval = epoll_wait(state->epfd, eventLoop->fired, eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
return retval;
}
核心邏輯:
主線程調(diào)用
epoll_wait阻塞等待就緒事件(無事件時(shí)休眠,消耗極低);epoll返回就緒的FD和事件,存入
eventLoop->fired數(shù)組;遍歷就緒事件數(shù)組,根據(jù)事件類型(讀/寫)調(diào)用對(duì)應(yīng)的回調(diào)函數(shù):
- 讀事件:
readQueryFromClient(讀取客戶端命令,解析并執(zhí)行); - 寫事件:
sendReplyToClient(將命令執(zhí)行結(jié)果返回給客戶端);
- 讀事件:
循環(huán)往復(fù),直到Redis停止。
4. 事件刪除(aeDeleteFileEvent)
當(dāng)客戶端斷開連接時(shí),Redis會(huì)刪除該FD的監(jiān)聽事件:
// ae.c
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask) {
if (fd >= eventLoop->setsize) return;
aeFileEvent *fe = &eventLoop->events[fd];
if (fe->mask == AE_NONE) return;
// 從epoll中刪除事件
aeApiDelEvent(eventLoop, fd, mask);
// 更新事件掩碼
fe->mask = fe->mask & (~mask);
if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
// 更新maxfd,優(yōu)化后續(xù)遍歷
while (eventLoop->maxfd >= 0 && eventLoop->events[eventLoop->maxfd].mask == AE_NONE)
eventLoop->maxfd--;
}
}
// ae_epoll.c
static int aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
int mask = eventLoop->events[fd].mask & (~delmask);
ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
int op = mask == AE_NONE ? EPOLL_CTL_DEL : EPOLL_CTL_MOD;
// 調(diào)用epoll_ctl刪除/修改事件
if (epoll_ctl(state->epfd, op, fd, &ee) == -1) return -1;
return 0;
}
四、核心設(shè)計(jì)亮點(diǎn)(源碼層面)
- 抽象層封裝:通過
aeApiCreate/aeApiAddEvent等接口,將epoll/kqueue/select的差異封裝,實(shí)現(xiàn)跨平臺(tái)兼容; - 數(shù)組映射FD:
eventLoop->events數(shù)組下標(biāo)直接對(duì)應(yīng)FD,無需遍歷查找,通過FD能O(1)找到對(duì)應(yīng)的事件對(duì)象; - 非阻塞IO:Redis在讀寫FD時(shí)均設(shè)置為非阻塞模式(
fcntl(fd, F_SETFL, O_NONBLOCK)),即使epoll通知就緒,讀寫也不會(huì)阻塞; - 事件復(fù)用:讀/寫事件可獨(dú)立注冊(cè)/刪除,比如執(zhí)行完命令后注冊(cè)寫事件,返回結(jié)果后刪除寫事件,避免無效觸發(fā)。
總結(jié)
- Redis IO多路復(fù)用的核心是aeEventLoop事件循環(huán)框架,封裝了epoll等底層多路復(fù)用接口,對(duì)外提供統(tǒng)一的事件注冊(cè)/處理能力;
- 源碼層面的核心流程:初始化事件循環(huán)→注冊(cè)FD事件→epoll_wait等待就緒→遍歷就緒事件→調(diào)用回調(diào)處理→循環(huán)往復(fù);
- 關(guān)鍵設(shè)計(jì):FD與事件數(shù)組下標(biāo)映射(O(1)查找)、跨平臺(tái)抽象層、非阻塞IO,保證了單線程處理海量連接的高效性。
這些源碼邏輯也是Redis單線程能支撐10萬+ QPS的核心——所有開銷都集中在“處理就緒事件”,無線程切換、無鎖競(jìng)爭(zhēng),且內(nèi)存操作極快。
到此這篇關(guān)于淺談Redis的IO多路復(fù)用的文章就介紹到這了,更多相關(guān)Redis IO多路復(fù)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis+IDEA實(shí)現(xiàn)單機(jī)鎖和分布式鎖的過程
這篇文章主要介紹了Redis+IDEA實(shí)現(xiàn)單機(jī)鎖和分布式鎖的過程,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
Redis?中ZSET數(shù)據(jù)類型命令使用及對(duì)應(yīng)場(chǎng)景總結(jié)(案例詳解)
這篇文章主要介紹了Redis?中ZSET數(shù)據(jù)類型命令使用及對(duì)應(yīng)場(chǎng)景總結(jié),本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-01-01
Redis中管道操作pipeline的實(shí)現(xiàn)
RedisPipeline是一種優(yōu)化客戶端與服務(wù)器通信的技術(shù),通過批量發(fā)送和接收命令減少網(wǎng)絡(luò)往返次數(shù),提高命令執(zhí)行效率,本文就來介紹一下Redis中管道操作pipeline的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Redis中SDS簡(jiǎn)單動(dòng)態(tài)字符串詳解
Redis中的SDS(Simple?Dynamic?String)是一種自動(dòng)擴(kuò)容的字符串實(shí)現(xiàn)方式,它可以提供高效的字符串操作,并且支持二進(jìn)制安全。SDS的設(shè)計(jì)使得它可以在O(1)時(shí)間內(nèi)實(shí)現(xiàn)字符串長(zhǎng)度的獲取和修改,同時(shí)也可以在O(N)的時(shí)間內(nèi)進(jìn)行字符串的拼接和截取。2023-04-04
redis不能訪問本機(jī)真實(shí)ip地址的解決方案
這篇文章主要介紹了redis不能訪問本機(jī)真實(shí)ip地址的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07

