Redis源碼閱讀【8-命令處理生命週期-2】

Redis源碼閱讀【1-簡單動態字符串】
Redis源碼閱讀【2-跳躍表】
Redis源碼閱讀【3-Redis編譯與GDB調試】
Redis源碼閱讀【4-壓縮列表】
Redis源碼閱讀【5-字典】
Redis源碼閱讀【6-整數集合】
Redis源碼閱讀【7-quicklist】
Redis源碼閱讀【8-命令處理生命週期-1】
Redis源碼閱讀【8-命令處理生命週期-2】
Redis源碼閱讀【8-命令處理生命週期-3】
Redis源碼閱讀【8-命令處理生命週期-4】
Redis源碼閱讀【番外篇-Redis的多線程】
建議搭配源碼閱讀源碼地址

1、介紹

  • Redis是典型的事件驅動型服務,而事件分爲文件事件socket的可獨寫事件)與時間事件(定時任務)兩大類。

2、事件處理

無論是文件事件還是時間事件都封裝在aeEventLoop中,代碼如下:

typedef struct aeEventLoop {
    int maxfd;   //已經接受的最大的文件描述符
    int setsize; //當前循環中所能容納的文件描述符的數量 
    long long timeEventNextId;//下一個時間事件的ID
    time_t lastTime;//上一次被訪問的時間,用來檢測系統時鐘是否被修改   
    aeFileEvent *events; //文件事件數組 
    aeFiredEvent *fired; //被觸發的文件事件 
    aeTimeEvent *timeEventHead; //事件實現的head,是一個鏈表
    int stop; //標識事件是否停止
    void *apidata; //對kqueue epoll select等封裝 類型是 aeApiState
    aeBeforeSleepProc *beforesleep;//用於阻塞的函數
    aeBeforeSleepProc *aftersleep;//用於喚醒的函數
    int flags; //事件類型標誌
} aeEventLoop;

stop 和 events標識事件循環是否結束,events爲文件事件數組,存儲已經註冊的文件事件;
fired存儲被觸發的文件事件;
timeEventHead Redis有多個定時任務,因此理論上應該有多個時間事件,多個時間事件形成鏈表,timeEventHead即爲時間事件鏈表頭節點;
beforesleep 和 aftersleep Redis服務器需要阻塞等待文件事件的發生,進程阻塞之前會調用beforesleep函數,進程因爲某種原因被喚醒之後會調用aftersleep函數;
apidata Redis底層可以使用4種I/O多路複用模型(kqueue epoll select等),apidata是對這4種模型的封裝,對應的類型是aeApiState
timeEventNextId 時間事件是一個鏈表,那麼timeEventNextId記錄的是下一個需要執行的時間事件ID;
lastTime 用來記錄上一次被訪問的時間,用來檢測系統時鐘是否被修改;
maxfd 已經接受的最大的文件描述符;
setsize當前循環中所能容納的文件描述符的數量 ;
flags用來標記當前事件類型,對應的枚舉內容如下:

//flags的定義
#define AE_FILE_EVENTS 1
#define AE_TIME_EVENTS 2
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4
#define AE_CALL_AFTER_SLEEP 8
  • 事件驅動程序一般來說都是一直循環執行下去的,沒錯Redis也是這樣,事件在一個循環裏面循環等待事件的發生並處理,當然這個循環不是死循環,是有條件的,代碼如下:
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0; //設置停止標記爲(不停止)
    while (!eventLoop->stop) { //除非停止標誌被設置,不然循環不會停止
        if (eventLoop->beforesleep != NULL) 
            eventLoop->beforesleep(eventLoop);//函數不爲空,先執行阻塞函數
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);//執行事件
    }
}
  • 函數aeProcessEvents爲事件處理主函數,其中第二個參數是一個標誌flagsAE_ALL_EVENTS標識函數需要處理文件事件與時間事件,AE_CALL_AFTER_SLEEP表示阻塞等待文件事件需要執行aftersleep函數。

3、文件事件

  • Redis客戶端通過TCP socket 與服務器端進行交互,文件事件指的就是socket的可讀可寫事件。socket獨寫操作有阻塞非阻塞之分。採用的是阻塞模式時,一個進程只能處理一條網絡連接的獨寫請求,爲了同時處理多條網絡連接,通常會採用多線程或者多進程的方式;非阻塞模式下,可以使用目前比較成熟的I/O多路複用模型,比如 select/epoll/kqueue等。視不同的操作系統而定。
  • 基於Linux系統,主要還是使用epollepollLinux內核爲處理大量併發網絡連接而提出的解決方案,能顯著提升操作系統CPU利用率 。eopll使用非常簡單,總共有3個API:epoll_create函數創建一個epoll專用的文件描述符,用於後續epoll相關API調用;epoll_ctl函數向epoll註冊,修改或刪除需要監控的事件;epoll_wait函數會阻塞進程,直到監控的若干網絡連接有事件發生。函數定義代碼如下:
//創建 epoll
int epoll_create (int __size);
//註冊 epoll事件
int epoll_ctl (int __epfd, int __op, int __fd,
		      struct epoll_event *__event);
//阻塞等待
int epoll_wait (int __epfd, struct epoll_event *__events,
		       int __maxevents, int __timeout)
  • epoll_create 輸入的size通知內核程序期望註冊的網絡連接數量,內核以此判斷初始分配空間大小;注意再Linux2.6.8版本之後,內核動態分配空間,此參數會被忽略。返回參數爲epoll專用的文件描述符,不再使用的時候需要關閉此文件描述符。

  • epoll_ctl函數執行成功返回0,否則返回-1,錯誤碼設置在變量errno裏面,輸入參數含義如下:

epfd 函數epoll_create返回的epoll文件描述符。
op 需要進行的操作,EPOLL_CTL_ADD表示註冊事件,EPOLL_CTL_MOD表示修改網絡連接事件,EPOLL_CTL_DEL表示刪除事件,事件定義如下:

#define EPOLL_CTL_ADD 1	/* Add a file descriptor to the interface.  */
#define EPOLL_CTL_DEL 2	/* Remove a file descriptor from the interface.  */
#define EPOLL_CTL_MOD 3	/* Change file descriptor epoll_event structure.  */

fd 網絡連接的socket文件描述符。
event 需要監控的事件,結構體epoll_event定義如下:

struct epoll_event
{
  uint32_t events;	//epoll 中包含的事件數組
  epoll_data_t data; //data 保存與文件描述符關聯的數據
} __EPOLL_PACKED;

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

  • 其中events表示需要監控的事件類型,比較常用的是EPOLLIN 文件描述符可讀事件,EPOLLOUT文件描述符可寫事件;data保存於文件描述關聯的數據。
    epoll事件定義如下:
enum EPOLL_EVENTS
  {
    EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
    EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
    EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
    EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
    EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
    EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
    EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
    EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
    EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
    EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
    EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
    EPOLLEXCLUSIVE = 1u << 28,
#define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
    EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
    EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
    EPOLLET = 1u << 31
#define EPOLLET EPOLLET
  };
  • epoll_wait 函數執行成功時返回0,否則返回-1,錯誤碼設置在變量errno;輸入參數含義如下:

epfd 函數epoll_create返回的epoll文件描述符;
epoll_event 作爲輸出參數使用,用於回傳已觸發的事件數組;
maxevents 每次能處理的最大事件數目;
timeout epoll_wait函數阻塞超時事件,如果超過timeout時間事件還沒發生,函數不再阻塞直接返回;當timeout設置爲0時函數立即返回,timeout設置爲-1時函數會一直阻塞到有事件發生;

  • Redis並沒有直接使用epoll提供的API,而是同時支持4種I/O多路複用模型,並將這些模型的API進一步統一封裝,由文件ae_evport.cae_epoll.cae_kqueue.cae_select.c 實現。

  • Redis在編譯的時候會檢查操作系統支持的I/O多路復模型來選擇使用哪種。

  • epoll爲例aeApiCreate函數是對epoll_create的封裝;aeApiAddEvent函數用於添加事件,是對epoll_ctl的封裝;aeApiDelEvent函數用於刪除事件,是對epoll_ctl的封裝;aeApiPoll是對epoll_wait的封裝。此外還有aeApiResizeaeApiFree,是綜合封裝用於擴展和完全釋放使用。函數定義如下:

static int aeApiCreate(aeEventLoop *eventLoop); 
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask);
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp); 
static int aeApiResize(aeEventLoop *eventLoop, int setsize);//調整事件循環的最大設置大小
static void aeApiFree(aeEventLoop *eventLoop);

這些函數定義的入參含義如下:
eventLoop 事件循環,與文件事件相關的最主要字段有三個,apidata指向I/O多路複用模型對象,注意4種I/O多路複用對象的類型不同,因此此字段是void*類型;events存儲需要監控的事件數組,以socket文件描述符作爲數組索引存取元素;fired存儲已觸發的事件數組。

  • epoll爲例,apidata字段指向的I/O多路複用模型對象定義如下:
typedef struct aeApiState {
    int epfd; //函數epoll_create返回的**epoll**文件描述符;
    struct epoll_event *events; //需要監控的事件類型
} aeApiState;
  • 其中epfd 函數epoll_create返回的epoll文件描述符,events存儲epoll_wait函數返回時已觸發的事件數組。
    fd 操作的socket文件描述符;
    mask 或 delmask 添加或者刪除的事件類型,AE_NONE表示沒有任何事件;AE_READABLE表示可讀事件;AE_WRITABLE表示可寫事件;
    tvp 阻塞等待文件事件的超時時間。

  • 正常運行的Redis服務器,aeApiPoll應該是調用最多的方法之一,現在來介紹一些其內部的具體實現:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
	//獲取對應的aeApiState類型
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    //阻塞等待事件的發生
    retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
                        tvp ? (tvp->tv_sec * 1000 + tvp->tv_usec / 1000) : -1);
    if (retval > 0) {
        int j;
        //所有發生的事件數量
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events + j;
            //轉換事件類型爲Redis定義的類型(比如:讀,寫等操作)參考 EPOLL_EVENTS枚舉
            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE | AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE | AE_READABLE;
            //記錄發生事件到fired數組(保存下來慢慢執行)
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}
  • 函數首先需要通過eventLoop->apidata 字段獲取 epoll模型對應的aeApiState結構體對象,才能調用epoll_wait函數等待事件的發生;epoll_wait函數將已觸發的事件存儲到aeApiState對象的events字段,Redis再次遍歷所有已觸發事件,將其封裝在eventLoop->fired數組,數組元素類型爲結構體aeFiredEvent,只有兩個字段,fd表示發生事件的socket文件描述符,mask表示發生的事件類型,如AE_READABLE可讀事件和AE_WRITABLE可寫事件。

  • 此外通過接口的對接我們可以切換不同類型的I/O多路複用模型,比如文件ae_select.cae_epoll.c中都實現了static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)這個方法。這種方式可以類比java裏面的接口,通過這種方式,我們可以無縫的切換各種類型的I/O多路複用模型:
    在這裏插入圖片描述

  • 上面簡單介紹了epoll的使用,以及Redisepoll等IO多路複用模型的封裝,下面我們回到本節的主題,文件事件。結構體aeEventLoop有一個關鍵字段events,類型爲aeFileEvent數組,存儲所有需要監控的文件事件。文件事件結構體定義如下:

typedef struct aeFileEvent {
    int mask; //存儲監控的文件事件類型 /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc; //函數指針,指向讀事件處理函數
    aeFileProc *wfileProc; //函數指針,指向寫事件處理函數
    void *clientData; //指向對應的客戶端對象
} aeFileEvent;

mask存儲監控的文件事件類型,如AE_READABLE可讀事件和AE_WRITABLE可寫事件。
rfileProc 函數指針,指向讀事件處理函數。
wfileProc 函數指針,指向寫事件處理函數。
clientData 指向對應的客戶端對象。

  • 調用aeApiAddEvent函數添加事件之前,首先需要調用aeCreateFileEvent函數創建對應的文件事件,並存儲在aeEventLoop結構體的events字段,aeCreateFileEvent函數實現如下:
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];//提取指針

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    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;
}

Redis服務器啓動時需要創建socket並監聽,等待客戶端連接;客戶端與服務器建立socket連接之後,服務器會等待客戶端的命令請求;服務器處理完客戶端的命令請求之後,命令回覆會暫時存在client結構體的buf緩衝區中,待客戶端文件描述符的可寫事件發生時,纔會真正往客戶端發送命令回覆。這些都需要創建對應的文件事件:

	//創建一個事件來監聽socket並使用acceptTcpHandler函數來處理
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                serverPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
  • 從Redis 5.0之後的新版本開始,接受客戶端請求的方法和響應客戶端的方法,readQueryFromClientsendReplyToClient分別在acceptTcpHandlerhandleClientsWithPendingReadsUsingThreads 中(handleClientsWithPendingReadsUsingThreadsafterSleep中有調用),同時Redis也開始採用多線程的方式去響應客戶端的請求,提高吞吐量。

  • 這裏同時引出一個問題,aeApiPoll函數的第二個參數是時間結構體timeval,存儲調用epoll_wait時傳入的超時時間,那麼這個時間是怎麼計算出來的呢?前面我們說過,Redis需要處理各種文件事件,同時還需要處理很多定時任務(時間事件),那麼當Redis由於執行epoll_wait而阻塞時,恰巧定時任務到期需要處理的時候怎麼辦?答案是:Redis會通過循環執行函數aeProcessEvents,在調用aeApiPoll之前遍歷Redis時間事件的鏈表,查找最終發生的時間事件,以此作爲aeApiPoll需要傳入的超時時間。代碼如下(aeProcessEvents函數實現):

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
    
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop); //查找最近發生的事件
        if (shortest) {
            long now_sec, now_ms;
            //獲取當前時間
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
        
            long long ms =
                (shortest->when_sec - now_sec)*1000 +
                shortest->when_ms - now_ms;

            if (ms > 0) {
                tvp->tv_sec = ms/1000;
                tvp->tv_usec = (ms % 1000)*1000;
            } else {
                tvp->tv_sec = 0;
                tvp->tv_usec = 0;
            }
        } else {
        
            //不需要等待,設置時間等待爲0
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                tvp = NULL; /* wait forever */
            }
        }

        if (eventLoop->flags & AE_DONT_WAIT) {
            tv.tv_sec = tv.tv_usec = 0;
            tvp = &tv;
        }
    
        //阻塞等待文件事件發生
        numevents = aeApiPoll(eventLoop, tvp);
       
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);

		//執行已經觸發的文件事件
        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 fired = 0; /* Number of events fired for current fd. */
            //處理文件事件,並且根據類型去執行 讀函數或者寫函數           
            //判斷是否爲等待一同處理類型的事件?
            int invert = fe->mask & AE_BARRIER;        
            //被觸發的讀事件
            if (!invert && fe->mask & mask & AE_READABLE) {
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
            }          
            //沒有被觸發的寫事件
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            //沒有被觸發的讀事件
            if (invert && fe->mask & mask & AE_READABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) {
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }
            processed++;
        }
    }

4、時間事件

  • 前面介紹了Redis文件事件,已經知道事件循環執行函數aeProcessEvents,時間事件的觸發和文件事件的觸發都在該函數中,其主要邏輯如下:

1、查找最早會發生的時間事件,計算超時時間
2、阻塞等待文件事件的產生
3、處理文件事件
4、處理時間事件,事件事件的處理函數爲processTimeEvents;

  • Redis服務器內部有很多定時任務需要執行,比如定時任務清楚超時客戶端連接,定時任務刪除過去鍵等等,定時任務被封裝爲時間時間aeTimeEvent對象,多個時間事件形成鏈表,存儲在aeEventLoop結構體的timeEventHead字段,它指向鏈表首節點。時間事件aeTimeEvent結構體定義如下:
typedef struct aeTimeEvent {
    long long id; //時間事件ID/* time event identifier. */
    long when_sec; //觸發的秒數/* seconds */
    long when_ms;  //觸發的毫秒數/* milliseconds */
    aeTimeProc *timeProc; //函數指針,指向時間事件的處理函數
    aeEventFinalizerProc *finalizerProc; //函數指針,刪除時間事件節點之前會先調用該函數(就是個鉤子)
    void *clientData;//指向對應客戶端對象
    struct aeTimeEvent *prev;//上一個節點
    struct aeTimeEvent *next;//下一個節點
} aeTimeEvent;

各字段含義如下:
id 時間事件唯一ID,通過字段eventLoop->timeEventNextId實現;
when_sec 和 when_ms 事件事件觸發的秒數與毫秒數;
timeProc 函數指針,指向時間事件處理函數;
finalizerProc 函數指針,刪除時間事件節點之前會調用此函數;
clientData 指向需要使用的對應客戶端數據;
prev 和 next 指向上一個或下一個事件事件鏈表節點;

  • 時間事件執行函數processTimeEvents的處理邏輯比較簡單,只是遍歷時間事件鏈表,判斷當前時間事件是否已經到期,如果到期執行時間事件處理函數timeProc如下:
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    time_t now = time(NULL);

    //防止ntp 時間同步帶來的時間回撥,這樣通過循環設置當前已經觸發的事件等待時間爲0
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }
    eventLoop->lastTime = now;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    while(te) {
        long now_sec, now_ms;
        long long id;
    
        //判斷該事件是否已需要刪除
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            if (te->prev)
                te->prev->next = te->next;
            else
                eventLoop->timeEventHead = te->next;
            if (te->next)
                te->next->prev = te->prev;
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            te = next;
            continue;
        }     
        //一個防禦校驗,防止當前事件的ID已經溢出,正常的事件ID是遞增的,防止認爲導致添加異常事件
        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        //獲取系統事件
        aeGetTime(&now_sec, &now_ms);
        //判斷當前事件是否到期,到期執行
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms))
        {
            int retval;

            id = te->id;
            //執行該事件事件 retval 是下次觸發該事件的時間
            retval = te->timeProc(eventLoop, id, te->clientData);
            processed++;
            //設置時間事件的到期時間
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        te = te->next;
    }
    return processed;
}
  • 事件函數timeProc的返回值retval,表示該事件下次被觸發的事件,單位爲秒,並且是一個相對時間(因爲任務可能執行的比久),即從當前時間算起,retval毫秒後此時間事件會被觸發。
  • 其實Redis只有一個時間事件,和文件事件一樣通過aeTimeEvent去封裝時間事件,再通過aeCreateTimeEvent去創建相應的時間事件,函數定義如下:
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc);

各個字段的含義如下:
eventLoop 輸入參數指向事件循環結構體;
milliseconds 表示此時間事件觸發時間,單位毫秒,注意這裏是一個相對時間,即從當前時間開始算起,milliseconds毫秒後會被觸發;
proc 指向時間事件的處理函數;
clientData 指向對應的結構體對象;
finalizerProc 函數指針,刪除事件的時候調用,相當於鉤子;

  • 通過全局搜索,會發現只有一個地方調用了(server.c文件,其它的是非業務主線功能,比如集羣同步相關的等等)aeCreateTimeEvent,只創建了一個時間事件,代碼如下:
	/* Create the timer callback, this is our way to process many background
     * operations incrementally, like clients timeout, eviction of unaccessed
     * expired keys and so forth. */
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }

該時間事件在1毫秒後觸發,處理函數爲serverCron,參數clientDatafinalizerProc都爲null,而函數serverCron實現了Redis服務所有的定時任務,進行週期指向,代碼如下:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
.......................................
 run_with_period(100) {
 //100毫秒週期執行
 }
 run_with_period(500) {
 //500毫秒週期執行
 }

 //清除超時客戶端連接
 clientsCron();
 //處理數據庫
 databasesCron();

 server.cronloops++;
 return 1000/server.hz;
}
  • 變量 server.cronloops用於記錄serverCron函數的執行次數,變量server.hz表示serverCron函數的執行頻率,用戶可以配置,最小爲1最大爲500,默認爲10。假設server.hz取默認值10,函數返回1000/server.hz,會更新當前時間事件的觸發時間爲100毫秒,即serverCron的執行週期爲100毫秒。run_with_period宏定義實現了定時任務按照指定時間週期(_ ms _)執行,此時會被替換爲一個if條件判斷,條件爲真纔會執行定時任務,定義如下:
#define run_with_period(_ms_) if ((_ms_ <= 1000/server.hz) || !(server.cronloops%((_ms_)/(1000/server.hz))))
  • 另外可以看到,serverCron函數會無條件執行某些定時任務,比如清除超時客戶端連接,以及處理數據庫等等(清除過期鍵等)。需要特別注意一點,serverCron函數的執行時間不能過長,否則會導致服務器不能及時響應客戶端的請求。下面以過期鍵刪除爲例子,分析Redis是如何保證serverCron函數的執行時間。過期鍵刪除由函數activeExpireCycle實現,由函數databasesCron調用,其函數實現如下:
void activeExpireCycle(int type) {
	timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
	static int timelimit_exit = 0;  
	............................................
	for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        unsigned long expired, sampled;

        redisDb *db = server.db+(current_db % server.dbnum);       
        current_db++;

      
        do {
        .......................................
            //查找並刪除過期鍵
            if ((iteration & 0xf) == 0) { 
                elapsed = ustime()-start;
                //檢測是否超時,超時要提前結束
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
           
        } while ((expired*100/sampled) > config_cycle_acceptable_stale);//判斷是否超時
    }
}
  • 函數activeExpireCycle最多遍歷dbs_per_call個數據庫,並記錄每個數據庫刪除的過期鍵數目;當刪除過期鍵數目大於最大限制時候,認爲數據庫過期鍵比較多,需要再次處理。考慮到極端情況,當數據庫鍵數目非常多且基本都過期的時候,do-while循環會一直執行下去。因此我們添加timelimit時間限制,每執行16次do-while循環,檢測函數activeExpireCycle執行時間是否超過timelimit,如果超過直接結束循環。
  • timelimit 的計算方式也是有講究的,timelimit計算出來的值要求讓activeExpireCycle函數的總執行時間佔CPU的25%,即每秒函數activeExpireCycle的總執行時間爲 1000000*25/100微秒。仍然假設server.hz默認值取10,即每秒函數activeExpireCycle執行10次,那麼每次函數調用的時間最多隻有 1000000*25/100/10單位微秒。

5、總結

  • 說到這裏,Redis的整個指令運行週期也講的差不多了,Redis是一個事件驅動的服務模型,根據上面的內容,我們其實大概可以總結出這樣的一個圖:
    在這裏插入圖片描述
    Redis的事件還多更加深入的實現,也歡迎大家繼續閱讀源碼,去挖掘Redis的內幕。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章