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
爲事件處理主函數,其中第二個參數是一個標誌flags
,AE_ALL_EVENTS
標識函數需要處理文件事件與時間事件,AE_CALL_AFTER_SLEEP
表示阻塞等待文件事件需要執行aftersleep
函數。
3、文件事件
- Redis客戶端通過TCP socket 與服務器端進行交互,文件事件指的就是socket的可讀可寫事件。socket獨寫操作有阻塞和非阻塞之分。採用的是阻塞模式時,一個進程只能處理一條網絡連接的獨寫請求,爲了同時處理多條網絡連接,通常會採用多線程或者多進程的方式;非阻塞模式下,可以使用目前比較成熟的I/O多路複用模型,比如 select/epoll/kqueue等。視不同的操作系統而定。
- 基於Linux系統,主要還是使用epoll。epoll是Linux內核爲處理大量併發網絡連接而提出的解決方案,能顯著提升操作系統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.c
,ae_epoll.c
,ae_kqueue.c
和ae_select.c
實現。 -
而Redis在編譯的時候會檢查操作系統支持的I/O多路復模型來選擇使用哪種。
-
以epoll爲例
aeApiCreate
函數是對epoll_create
的封裝;aeApiAddEvent
函數用於添加事件,是對epoll_ctl
的封裝;aeApiDelEvent
函數用於刪除事件,是對epoll_ctl
的封裝;aeApiPoll
是對epoll_wait
的封裝。此外還有aeApiResize
和aeApiFree
,是綜合封裝用於擴展和完全釋放使用。函數定義如下:
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.c
和ae_epoll.c
中都實現了static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
這個方法。這種方式可以類比java裏面的接口,通過這種方式,我們可以無縫的切換各種類型的I/O多路複用模型:
-
上面簡單介紹了epoll的使用,以及Redis對epoll等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之後的新版本開始,接受客戶端請求的方法和響應客戶端的方法,
readQueryFromClient
和sendReplyToClient
分別在acceptTcpHandler
和handleClientsWithPendingReadsUsingThreads
中(handleClientsWithPendingReadsUsingThreads
在afterSleep
中有調用),同時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
,參數clientData
和finalizerProc
都爲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的內幕。