redis command 执行过程的代码分析

本文通过命令从client端到server端的执行过程, 分析一下命令的处理过程。

redisContext

对于每一个连接, 会产生一个redisContext, 记录了连接的上下文信息, 比如:sock fd, 写入的buf, 以及sockaddr等。 这里,使用的是hiredis里面的接口和数据结构, 也就是说数据从client到server之间的传输使用了hiredis C driver的功能。

也就是说, 不管是通过redis-cli或者客户应用程序通过redis 客户端driver程序接口, 都是通过hiredis(C接口, 其他语言的driver类似)将指令传达到redis服务器。

redis socket 事件处理机制

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那种, 将命令分成几种类型, 并且分别调用到不同的接口来处理会显得更加清晰一点。

你可能感兴趣的:(redis)