原文地址:www.xilidou.com/2018/03/22/…
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 總是一個或多或少會問到的問題。
讀完這篇文章你也許就能回答這幾個問題:
- 為什麼 Redis 是一個單執行緒應用?
- 為什麼 Redis 是一個單執行緒應用,卻有如此高的效能?
如果你用本文提供的知識點回答這兩個問題,一定會在面試官心中留下一個高大的形象。
大家還可以閱讀我的 Redis 相關的文章:
Redis 的基礎資料結構(二) 整數集合、跳躍表、壓縮列表
歡迎關注我的微信公眾號: