小白談一談:命令在redis中是怎麼被執行的呢?

easyer 發表於 2021-03-08

當我準備談談redis的命令是如何執行時,發現其實在網上已經有很多優秀的文章講過這類問題了。
推薦看過的一個github上的原始碼分析:github.com/menwengit/redis_source_...

當然,即使很多人已經講解了這類問題,可是依然不影響我去寫一些自己的所感。因為,讀書百遍不如自己寫一遍。這也是防止眼高手低。

命令是如何被執行的呢?

以在命令列執行命令redis-cli為例。這個我們太熟悉了,只要執行redis-cli就連線redis-server成功了,接下來就可以執行一些getset命令。再然後就得到了我們要的結果。但是你從來有想過,這些命令的背後到底幹了些什麼呢?接下里是我翻看原始碼(版本3.0.7)之後,得到的一些答案。

第一步:啟動伺服器

檔案入口:redis.cmain函式
重點看如下的2個方法:

initServer();
aeMain(server.el);

initServer中初始化的資料很多,其中和本節討論相關的有:

  • 初始化server的資料儲存結構
  • 初始化服務端socket
  • 初始化EventLoop

初始化server的資料儲存結構是貫穿redis的生命週期的,因為任何操作都會涉及到從redis的資料結構中獲取或者設定資料。
初始化服務端socket
1)在server的配置檔案中,是支援多個socket的配置。也支援IPv4和IPv6。這麼做的好處就是redis-server既可以支援本地連線的unix,又支援本地的網路連線127.0.0.1,還能支援遠端的網路連線remote_address:port等。
2)預設的socket初始化後設定的是非阻塞模式。
初始化EventLoop

對於小白的我來說,剛學redis聽得最多的就是redis中的事件,之所以redis很快就是因為事件和IO多路複用及單程式等等。聽的再多,如果不自己去探究原始碼終究是紙上得來終覺淺,絕知此事要躬行。所以即便是看完了本篇文章依然要去看原始碼,每個人的理解都會不一樣。我本地是mac,redis底層自動會使用kqueue。以下的分析都是基於kqueue的IO多路複用。

// 初始化 EventLoop物件
server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
// 註冊時間事件到el物件,回撥函式是`serverCron`
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
// 註冊網路連線的檔案事件:就是服務端socket監聽client的連線,並且關聯fd和回撥函式`acceptTcpHandler`。
for (j = 0; j < server.ipfd_count; j++) {
    aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL)
}
// 註冊本地檔案事件:監聽本地client的連線並且關聯回撥函式`acceptUnixHandler`。
aeCreateFileEvent(server.el,server.sofd,AE_READABLE,acceptUnixHandler,NULL);

到這裡,將服務端的socket和時間事件及檔案事件都簡單的拉出來溜溜了。怎麼理解kqueue的事件觸發呢?聯想PHP的socket_select函式去理解。推薦閱讀:www.jianshu.com/p/397449cadc9a

  • socket_select函式是很早之前最開始使用的IO多路複用模式,它的工作機制就是在核心態接收到的事件通知給使用者態,並且返回目前已準備好的數量。然後切換到使用者態之後在迴圈已經準備好的數量,接收資料。
    socket_select($read, $write, $except, $sec, $usec);
  • $read是可讀事件:將所有可讀的事件放到該陣列,然後從該陣列迴圈遍歷並且讀取陣列內檔案的資料。
  • $write是可寫事件:將已準備好的可寫檔案放在這陣列,然後迴圈遍歷呼叫寫函式寫入資料到該檔案。

select的缺點就是迴圈遍歷,並且最大隻能支援1024個客戶端同時連線。後來升級到了poll,但是poll只是去除了1024的限制。後來直到進化到了epoll,才得以解決select的所有弊端,kqueue和epoll類似。

都知道一個問題:Redis是序列的,那為什麼速度還那麼快呢?一般我們的回答都是IO多路複用。那麼現在有個問題就是:Redis是如何保證序列的呢?

當你讀完Redis序列地處理client的請求時,可能並沒有多大感觸。但是如果讓你寫一個基於多程式下的server和client通訊時,你就會發現如何保證server和client接收到的資料準確可信是很難的問題。這裡就不得不讚嘆為什麼Redis要用單程式處理了,因為實在是省去了很多併發問題的思考。
多程式下的serverclient通訊問題,看beanstalk的issue:github.com/pheanstalk/pheanstalk/i...

解答這個問題必須再看原始碼的時候,注意aeEventLoop的資料結構:

/* State of an event based program */
// 事件狀態的描述
typedef struct aeEventLoop {
    // 最大的事件描述符
    int maxfd;   /* highest file descriptor currently registered */
    // 目前追蹤的事件數量
    int setsize; /* max number of file descriptors tracked */
    // 時間事件的下一個待觸發的id
    long long timeEventNextId;
    // 用於檢測系統偏差
    time_t lastTime;     /* Used to detect system clock skew */
    // 已註冊的檔案事件:如服務端socket。是一個array。
    aeFileEvent *events; /* Registered events */
    // 觸發的事件,是一個array。
    aeFiredEvent *fired; /* Fired events */
    // 時間事件是一個連結串列。指向了該連結串列的頭指標。
    aeTimeEvent *timeEventHead;
    // 置為1則結束迴圈
    int stop;
    // 萬能指標 void*:
    // 我的mac是使用的是kqueue,所以儲存的資料結構如:
    /*typedef struct aeApiState {
            int kqfd; //指的是kqueue
            struct kevent *events; //提前申請好的setsize的kqueue的大小空間
        } aeApiState;*/
    void *apidata; /* This is used for polling API specific data */
    // 回撥函式
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

先注意到兩個屬性:aeFileEvent *eventsaeFiredEvent *fired,然後對比下ae.c檔案下的aeCreateFileEvent函式去了解。

// accept:server.el  server.ipfd[j]   AE_READABLE   acceptTcpHandler  null
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    // 已註冊的事件,這裡是fe是引用的地址。
    aeFileEvent *fe = &eventLoop->events[fd];
    // 註冊kevent事件:readable | writeable
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    //如 accept是READBALE事件
    fe->mask |= mask;
    // rfileproc和wfileproc分別記錄回撥函式,和fd關聯起來了
    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;
}

結合之前的accept操作:server.el server.ipfd[j] AE_READABLE acceptTcpHandler null,那麼得到的如下:

/* File event structure
typedef struct aeFileEvent {
    int mask;  //one of AE_(READABLE|WRITABLE)
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} aeFileEvent; */

aeFileEvent *events[fd1] = {
    int mask = AE_READABLE;
    aeFileProc *rfileProc = 'acceptTcpHandler';
    aeFileProc *wfileProc;
    void *clientData = null;
}; /* Registered events */
aeFiredEvent *fired; /* Fired events */

同時aeApiAddEventkevent註冊了檔案事件,程式碼如下:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct kevent ke;
    if (mask & AE_READABLE) {
        EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
        /* kevent引數說明
        kq            - kqueue() 返回的唯一描述符, 標記著一個核心佇列
        changes       – 需要對kqueue進行修改的事件集合, 此引數就是kevent()對目前kqueue中的事件的操作,比如刪除kqueue中已經存在的事件,或者向kqueue中新增新的事件,也就是說,kevent()通過此引數對kqueue的修改
        nchanges      – 需要修改的事件的個數
        events        – kevent()會把所有事件儲存在events中
        nevents       – kevent()需要知道儲存空間有多大, == 0 : kevent()會立即返回
        timeout       – 超時控制, = NULL:kevent()會一直等到有關注的事件發生; != NULL:kevent()會等待指定的時間
        */
        // 結合select理解:就是新增到了read陣列,並且等待資料的讀取。
        if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
    }
    if (mask & AE_WRITABLE) {
        EV_SET(&ke, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
        // 結合select理解:就是新增到了write陣列,並且等待資料的寫入。
        if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
    }
    return 0;
}

當事件被觸發時,那麼就會觸發執行回撥函式acceptTcpHandler,簡練的程式碼如下:

// 其實就是執行了`accept`函式,並且返回了client的檔案描述符cfd。
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
// 設定成非阻塞模式,且將cfd註冊到kevent:
// aeCreateFileEvent(server.el,fd,AE_READABLE, readQueryFromClient, c)
acceptCommonHandler(cfd,0);

那麼當client請求server傳送命令時,就會觸發函式readQueryFromClient,Redis經過對命令一系列的處理之後,經過函式processCommand執行命令得到結果。但是並不會立即返回給client,而是將事件註冊到了kevent:aeCreateFileEvent(server.el, c->fd, AE_WRITABLE, sendReplyToClient, c)
這裡可想可知EventLoop的屬性event又多加了一個事件。
此時就能得到如下的EventLoop的部分屬性資料:

aeFileEvent 
events[fd1] = {
    int mask = AE_READABLE;
    aeFileProc *rfileProc = 'acceptTcpHandler';
    aeFileProc *wfileProc;
    void *clientData = null;
}; /* Registered events */
event[fd2] = {
    int mask = AE_READABLE;
    aeFileProc *rfileProc = 'readQueryFromClient';
    aeFileProc *wfileProc;
    void *clientData = c;
};
event[fd2] = {
    int mask = AE_READABLE;
    aeFileProc *rfileProc = 'sendReplyToClient';
    aeFileProc *wfileProc;
    void *clientData = c;
};
aeFiredEvent *fired; /* Fired events */

以上都只是介紹了事件被觸發後的執行過程,但是這些事件是何時被觸發的呢?這些事件的觸發在main函式的aeMain函式中,跟蹤程式碼如:

...
// 和時間事件聯合起作用,可能等待至下一個時間事件的觸發,也可能等待直到某個事件發生。
// 一旦某個事件被觸發,就會將該事件記錄到`EventLoop`的`fired`屬性。然後非同步呼叫了回撥函式。
numevents = aeApiPoll(eventLoop, tvp);
// 這就是序列的根本原因所在:將所有觸發的事件先儲存到陣列`fired`,然後在遍歷這些事件依次處理。
// 由於此前已經將fd和回撥函式相關聯,則在遍歷的時候就可以執行回撥函式。
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;

        /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            if (fe->mask & mask & AE_READABLE) {
                rfired = 1;
                /* 如socket等待accept,那麼就是一個READABLE事件,然後執行的回撥函式
                 * rfileProc=acceptTcpHandler,那麼就會執行這個函式 */
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    // 如get命令,就是一個writeable事件
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;
        }

看下aeApiPoll函式:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    if (tvp != NULL) {
        struct timespec timeout;
        timeout.tv_sec = tvp->tv_sec;
        timeout.tv_nsec = tvp->tv_usec * 1000;
        // 時間事件存在時,則等待下個時間事件的觸發的時間。如果此區間觸發了一個檔案事件,
        // 則優先處理檔案事件。時間事件可能就會延遲處理。
        retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
                        &timeout);
    } else {
        // 阻塞等待直到事件的發生
        retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
                        NULL);
    }

    if (retval > 0) {
        int j;
        numevents = retval;
        // 檔案事件被觸發了,優先執行檔案事件
        for(j = 0; j < numevents; j++) {
            int mask = 0;
            struct kevent *e = state->events+j;
            // 將kevent監聽到的事件型別和eventLoop的mask相關聯。
            if (e->filter == EVFILT_READ) mask |= AE_READABLE;
            if (e->filter == EVFILT_WRITE) mask |= AE_WRITABLE;
            // 這就是序列,是一個陣列
            eventLoop->fired[j].fd = e->ident;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

到這裡,在整體的過一遍就是:

  • server註冊服務端socket的fd到kevent,然後kevent就會非同步的去監聽這個fd,並且將這個fd關聯回撥函式acceptTcpHandler
  • kevent監聽到了client的連線請求,然後呼叫了函式acceptTcpHandler,然後再把cfd註冊到了kevent且關聯了回撥函式readQueryFromClient
  • kevent監聽到了client的傳送資料,然後呼叫了函式readQueryFromClient,再處理請求將cfd註冊到了kevent且關聯了回撥函式sendReplyToClient

時間事件和檔案事件:
時間事件的觸發分為定時事件和週期性事件,定時事件觸發一次就結束了,而週期性事件會每隔固定的時間就被觸發一次,如serverCron
在時間事件觸發的時間窗內,就會觸發kevent阻塞相應的等待時間。在這個時間視窗內,可能就會觸發了某個檔案事件。如果沒有觸發檔案事件,則會執行時間事件。
kqueue介紹

第二步:連線伺服器

分析原始檔redis-cli.cmain函式。

/* Start interactive mode when no command is provided */
    if (argc == 0 && !config.eval) {
        /* Ignore SIGPIPE in interactive mode to force a reconnect */
        signal(SIGPIPE, SIG_IGN);

        /* Note that in repl mode we don't abort on connection error.
         * A new attempt will be performed for every command send. */
         // client是阻塞模式
        cliConnect(0);
        repl();
    }

第三步:get or set

cliSendCommand(argc,argv,repeat);
本作品採用《CC 協議》,轉載必須註明作者和本文連結