Redis 中的事件驅動模型

原文地址:www.xilidou.com/2018/03/22/…react

Redis 是一個事件驅動的內存數據庫,服務器須要處理兩種類型的事件。linux

  • 文件事件
  • 時間事件

下面就會介紹這兩種事件的實現原理。面試

文件事件

Redis 服務器經過 socket 實現與客戶端(或其餘redis服務器)的交互,文件事件就是服務器對 socket 操做的抽象。 Redis 服務器,經過監聽這些 socket 產生的文件事件並處理這些事件,實現對客戶端調用的響應。redis

Reactor

Redis 基於 Reactor 模式開發了本身的事件處理器。數據庫

這裏就先展開講一講 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 是一個鏈表結構。看圖:

timeEvent

注意這是一個按照id倒序排列的鏈表,並無按照事件順序排序。

processTimeEvent

Redis 使用這個函數處理全部的時間事件,咱們整理一下執行思路:

  1. 記錄最新一次執行這個函數的時間,用於處理系統時間被修改產生的問題。
  2. 遍歷鏈表找出全部 when_sec 和 when_ms 小於如今時間的事件。
  3. 執行事件對應的處理函數。
  4. 檢查事件類型,若是是週期事件則刷新該事件下一次的執行事件。
  5. 不然從列表中刪除事件。

綜合調度器(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 的基礎數據結構(一) 可變字符串、鏈表、字典

Redis 的基礎數據結構(二) 整數集合、跳躍表、壓縮列表

Redis 的基礎數據結構(三)對象

Redis 數據庫、鍵過時的實現

歡迎關注個人微信公衆號:

二維碼
相關文章
相關標籤/搜索