本文通过命令从client端到server端的执行过程, 分析一下命令的处理过程。
对于每一个连接, 会产生一个redisContext, 记录了连接的上下文信息, 比如:sock fd, 写入的buf, 以及sockaddr等。 这里,使用的是hiredis里面的接口和数据结构, 也就是说数据从client到server之间的传输使用了hiredis C driver的功能。
也就是说, 不管是通过redis-cli或者客户应用程序通过redis 客户端driver程序接口, 都是通过hiredis(C接口, 其他语言的driver类似)将指令传达到redis服务器。
redis指令到达服务端, 是通过注册的事件来通知服务器进行处理的,每当有新的消息到达, 会根据之前注册过的事件来进行处理。
在redis-server启动的时候, 对每一个绑定的IP,会注册一个事件来处理每一个到达的信号。
void initServer(void) {
...
//为每一个绑定的IP,创建一个TCP连接
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
// 每一个TCP连接有信号到达, 触发AE_READABLE事件, 并且调用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.");
}
}
...
}
这里, acceptTcpHandler是一个通用的信号处理函数, 在内部通过redisClient来创建一个链接的上下文, 由于有多种连接上下文, 比如, 真实的外来连接, 备份时的连接, LUA脚本建立的连接等。因此, 在创建redisClient的时候, 又设定了具体注册信号和函数。
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
acceptCommonHandler(cfd,0,cip);
}
}
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;
}
...
}
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
// fd != -1说明是真正的远端tcp连接,其处理函数为readQueryFromClient
if (fd != -1) {
anetNonBlock(NULL,fd);
anetEnableTcpNoDelay(NULL,fd);
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}
...
}
在readQueryFromClient里面, 针对不同的信号类型, 进行不同的处理方式。
通过redisCommandTable来指定当前redis版本支持的command的类型, 以及每一种类型的一些设定, 每一种命令, 都有一个redisCommand结构体来记录相关的信息:
struct redisCommand {
// coomand name, 用来查从server.commands找RedisCommand项
char *name;
// 该command的响应处理函数
redisCommandProc *proc;
// 参数个数
int arity;
// 该命令的flag设定, 分别为字符串形式和bit-OR形式
char *sflags;
uint64_t flags;
// cluster 模式下, 通过key来获得相关参数
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// 统计信息: command 执行时间以及被调用次数
long long microseconds, calls;
// 单调递增的command id
int id;
};
在server结构里面, 有commands字段的字典来记录所有支持的command以及该command对应的参数信息和处理函数。这里, 由于所有的命令走的一样的流程, 函数的调用链比较长, 而且每一个调用里面有很多的校验, 我们只是列出了主要流程, 相关的错误校验以及处理就忽略了。
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
nread = read(fd, c->querybuf+qblen, readlen);
processInputBufferAndReplicate(c);
}
void processInputBufferAndReplicate(client *c) {
if (!(c->flags & CLIENT_MASTER)) {
processInputBuffer(c);
} else {
size_t prev_offset = c->reploff;
processInputBuffer(c);
size_t applied = c->reploff - prev_offset;
if (applied) {
replicationFeedSlavesFromMasterStream(server.slaves,
c->pending_querybuf, applied);
sdsrange(c->pending_querybuf,applied,-1);
}
}
}
void processInputBuffer(client *c) {
...
processCommand(c);
...
}
int processCommand(client *c) {
// 从字典里面通过command名字找到相关的redisCommand
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
//权限认证的校验
if (auth_required || DefaultUser->flags & USER_FLAG_DISABLED) {
/* AUTH and HELLO are valid even in non authenticated state. */
if (c->cmd->proc != authCommand || c->cmd->proc == helloCommand) {
flagTransaction(c);
addReply(c,shared.noautherr);
return C_OK;
}
}
// ACL 校验
int acl_retval = ACLCheckCommandPerm(c);
if (acl_retval != ACL_OK) {
flagTransaction(c);
return C_OK;
}
// cluster redirect 校验
if (server.cluster_enabled &&
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
int error_code;
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,
&hashslot,&error_code);
if (n == NULL || n != server.cluster->myself) {
if (c->cmd->proc == execCommand) {
discardTransaction(c);
} else {
flagTransaction(c);
}
clusterRedirectClient(c,n,hashslot,error_code);
return C_OK;
}
}
// 错误校验代码
/* Exec the command */
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);
c->woff = server.master_repl_offset;
if (listLength(server.ready_keys))
handleClientsBlockedOnKeys();
}
return C_OK;
}
最后, 除了MULTI/EXEC外, 会走进call函数, 并且调用c->cmd->proc()来执行。
其实, 将所有的命令使用同一套流程来处理, 显得流程很臃肿, 真正执行到的代码并不是很多, 也许采用类似MongoDB那种, 将命令分成几种类型, 并且分别调用到不同的接口来处理会显得更加清晰一点。