當我準備談談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
執行命令,最終執行的程式碼如:c->cmd->proc(c);
,這裡的函式就是執行命令的函式,如set|get
等。執行完命令之後透過addReply
函式返回給客戶端結果。但是並不會立即返回給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
。
更具體的描述是:
- 在
initServer
中先初始化server.el
,這是全域性公用的。server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);
- 透過
aeCreateTimeEvent
註冊時間事件。這裡只是註冊,該函式會立即返回。aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL)
- 然後將初始化好的服務端Socket也透過
aeCreateFileEvent
註冊檔案事件。aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL); aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,NULL);
- 然後在
aeMain
函式中,處理時間事件和檔案事件。void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; // 事件處理是一個迴圈 while (!eventLoop->stop) { // 在此觸發beforesleep函式。 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
aeProcessEvents
函式會先執行時間事件,在連結串列timeEventHead
找到最近的時間事件。
```php
/* … 省略上述程式碼
找到最近的時間事件並且計算下次執行的時間間隔。但是可以看到時間事件的優先順序是低於檔案事件的。
即使時間事件已經到了該執行的時間,可是也會先呼叫kevent
函式等待檔案事件的發生。不過這裡分為兩種情況:
1)無時間事件:kevent會一直阻塞等待直到檔案事件發生。
2)有時間事件:kevent會等待最近時間事件觸發的時間間隙去處理檔案事件。如果沒有檔案事件發生,則處理時間事件。aeApiPoll
函式內,如果有檔案事件觸發,則將fd
放到aeEventLoop->aeFiredEvent
的fired
陣列,並且關聯AE_READABLE|AE_WRITABLE。
*/
numevents = aeApiPoll(eventLoop, tvp);
- 迴圈`aeEventLoop->aeFiredEvent`的`fd`,並且關聯起`aeEventLoop->aeFileEvent`的`event`,取出`fd`的對應關係`event[fd]`的操作函式`proc`即之前註冊事件時的回撥函式,然後開始執行回撥函式`proc`。如此所有kevent觸發的事件都會被序列放在`fired`陣列,然後在依次執行這些`fd`的回撥函式。
> 時間事件和檔案事件:
時間事件的觸發分為定時事件和週期性事件,定時事件觸發一次就結束了,而週期性事件會每隔固定的時間就被觸發一次,如`serverCron`。
在時間事件觸發的時間窗內,就會觸發`kevent`阻塞相應的等待時間。在這個時間視窗內,可能就會觸發了某個檔案事件。如果沒有觸發檔案事件,則會執行時間事件。
[kqueue介紹](https://blog.csdn.net/bytxl/article/details/17526351 "kqueue介紹")
## 第二步:client
分析原始檔`redis-cli.c`的`main`函式。
```c
/* 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();
}
在repl()
的程式碼繼續跟蹤到redisGetReply
,再到redisBufferWrite
方法,是寫資料到server。
int redisGetReply(redisContext *c, void **reply) {
int wdone = 0;
void *aux = NULL;
/* Try to read pending replies */
if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
return REDIS_ERR;
/* For the blocking context, flush output buffer and read reply */
if (aux == NULL && c->flags & REDIS_BLOCK) {
/* Write until done */
do {
if (redisBufferWrite(c,&wdone) == REDIS_ERR)
return REDIS_ERR;
} while (!wdone);
/* Read until there is a reply */
do {
if (redisBufferRead(c) == REDIS_ERR)
return REDIS_ERR;
if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
return REDIS_ERR;
} while (aux == NULL);
}
/* Set reply object */
if (reply != NULL) *reply = aux;
return REDIS_OK;
}
int redisBufferWrite(redisContext *c, int *done) {
int nwritten;
/* Return early when the context has seen an error. */
if (c->err)
return REDIS_ERR;
if (sdslen(c->obuf) > 0) {
nwritten = write(c->fd,c->obuf,sdslen(c->obuf));
if (nwritten == -1) {
// errno==35 || errno == 4 這不是個錯誤,繼續執行。注意到client是阻塞模式。
if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
/* Try again later */
} else {
__redisSetError(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
} else if (nwritten > 0) {
if (nwritten == (signed)sdslen(c->obuf)) {
sdsfree(c->obuf);
c->obuf = sdsempty();
} else {
sdsrange(c->obuf,nwritten,-1);
}
}
}
if (done != NULL) *done = (sdslen(c->obuf) == 0);
return REDIS_OK;
}
讀取server的資料程式碼如:
/* Use this function to handle a read event on the descriptor. It will try
* and read some bytes from the socket and feed them to the reply parser.
*
* After this function is called, you may use redisContextReadReply to
* see if there is a reply available. */
int redisBufferRead(redisContext *c) {
char buf[1024*16];
int nread;
/* Return early when the context has seen an error. */
if (c->err)
return REDIS_ERR;
nread = read(c->fd,buf,sizeof(buf));
if (nread == -1) {
if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
/* Try again later */
} else {
__redisSetError(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
} else if (nread == 0) {
__redisSetError(c,REDIS_ERR_EOF,"Server closed the connection");
return REDIS_ERR;
} else {
if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) {
__redisSetError(c,c->reader->err,c->reader->errstr);
return REDIS_ERR;
}
}
return REDIS_OK;
}
思考
分析完Redis的kevent之後,聯想了下Swoole
的協程
,如程式碼:
Co\run(function() {
go(function() {
Co::sleep(1);
// do something
});
});
本作品採用《CC 協議》,轉載必須註明作者和本文連結