當我準備談談redis的命令是如何執行時,發現其實在網上已經有很多優秀的文章講過這類問題了。
推薦看過的一個github上的原始碼分析:github.com/menwengit/redis_source_...
當然,即使很多人已經講解了這類問題,可是依然不影響我去寫一些自己的所感。因為,讀書百遍不如自己寫一遍。這也是防止眼高手低。
命令是如何被執行的呢?
以在命令列執行命令redis-cli
為例。這個我們太熟悉了,只要執行redis-cli
就連線redis-server
成功了,接下來就可以執行一些get
或set
命令。再然後就得到了我們要的結果。但是你從來有想過,這些命令的背後到底幹了些什麼呢?接下里是我翻看原始碼(版本3.0.7)之後,得到的一些答案。
第一步:啟動伺服器
檔案入口:redis.c
的main
函式
重點看如下的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
要用單程式處理了,因為實在是省去了很多併發問題的思考。
多程式下的server
和client
通訊問題,看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 *events
和 aeFiredEvent *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 */
同時aeApiAddEvent
往kevent
註冊了檔案事件,程式碼如下:
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.c
的main
函式。
/* 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 協議》,轉載必須註明作者和本文連結