我们在使用Redis时,都是向Redis发送各种命令,然后接受命令的返回数据。但是每个命令的执行过程是怎样的呢?Redis怎么处理每个命令的呢?
在正式进入命令执行流程分析之前,我们先要了解一下Redis的启动过程。
Redis的启动
我们只看单机Redis的启动流程,哨兵、集群、module等都先跳过,这样使我们分析起来更简单。就算简化了分析过程,Redis启动时也做了一堆的事,限于本人文采不行,还是直接上图吧。
图中省略了很多,但不影响我们接下来的分析。
大家都知道,Redis通过事件来处理网络IO,从图中可以看到,Redis通过aeCreateFileEvent函数创建TCP handler事件。
void initServer(void) {
...
/* Create an event handler for accepting new connections in TCP and Unix
* domain sockets. */
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.");
}
}
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");
...
}
我们只分析TCP事件,忽略Unix domain。可以看到TCP事件的处理句柄为acceptTcpHandler。
连接Redis
当我们通过下面的命令连接Redis时:
$ redis-cli -h host -p port -a password
TCP连接事件被触发,acceptTcpHandler事件句柄被调用。
acceptTcpHandler
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
...
// 接受客户端的请求
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
...
// 处理客户端的请求
acceptCommonHandler(cfd,0,cip);
...
}
acceptCommonHandler
static void acceptCommonHandler(int fd, int flags, char *ip) {
client *c;
// 创建新的客户端
if ((c = createClient(fd)) == NULL) {
serverLog(LL_WARNING,
"Error registering fd event for the new client: %s (fd=%d)",
strerror(errno),fd);
close(fd); /* May be already closed, just ignore errors */
return;
}
...
}
服务端状态server会保存每个连接的客户端信息,
struct redisServer {
...
list *clients; /* List of active clients */
...
}
createClient
// 新建客户端,设置客户端事件,并将客户端信息保存到server.clients队列中
client *createClient(int fd) {
...
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
...
}
这里,又新建了个IO事件,用来读取客户端发送的命令,事件的句柄为readQueryFromClient。
发送命令
redis-cli> set mykey hello
当我们向Redis发送这条命令时,readQueryFromClient句柄被触发。
readQueryFromClient
readQueryFromClient读取客户端发送的命令。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
// 读取客户端发送的命令,并保存到缓存中
...
processInputBufferAndReplicate(c);
}
processInputBufferAndReplicate
void processInputBufferAndReplicate(client *c) {
if (!(c->flags & CLIENT_MASTER)) {
processInputBuffer(c);
} else {
// 如果是master服务,先忽略
...
}
}
processInputBuffer
void processInputBuffer(client *c) {
...
while(c->qb_pos < sdslen(c->querybuf)) {
... // 前置判断,忽略
// 从缓存中读取命令,并解析Redis协议
// https://redis.io/topics/protocol Redis协议描述
// 这里只需要知道知道函数的功能就行,不需要详细了解具体的实现。
if (c->reqtype == PROTO_REQ_INLINE) {
if (processInlineBuffer(c) != C_OK) break;
} else if (c->reqtype == PROTO_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != C_OK) break;
} else {
serverPanic("Unknown request type");
}
...
if (processCommand(c) == C_OK) {
...
}
...
}
}
processCommand
processCommand执行命令,
/* If this function gets called we already read a whole
* command, arguments are in the client argv/argc fields.
* processCommand() execute the command or prepare the
* server for a bulk read from the client.
*
* If C_OK is returned the client is still alive and valid and
* other operations can be performed by the caller. Otherwise
* if C_ERR is returned the client was destroyed (i.e. after QUIT). */
int processCommand(client *c) {
...
/* Now lookup the command and check ASAP about trivial error conditions
* such as wrong arity, bad command name and so forth. */
// 从命令字典表中查找该命令,并设置命令的处理函数
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
...
/* Exec the command */
// 执行命令
// 只关注call函数,其他先注释掉。
//if (c->flags & CLIENT_MULTI &&
// c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
// c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
//{
// queueMultiCommand(c);
// addReply(c,shared.queued);
//} else {
call(c,CMD_CALL_FULL); // 只关注call这里
// c->woff = server.master_repl_offset;
// if (listLength(server.ready_keys))
// handleClientsBlockedOnKeys();
//}
return C_OK;
}
call执行具体的命令实现函数。之后只需要关注没个命令的具体实现就行了。
后记
命令处理流程调用栈大致如下
acceptTcpHandler
acceptCommonHandler
createClient
readQueryFromClient
processInputBufferAndReplicate
processInputBuffer
processInlineBuffer
processCommand
lookupCommand
call
具体的命令实现
Redis的命令实现函数,都有固定的命名格式的void xxxCommand,所以大家可以在源码目录src下直接搜索。如set命令: