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那種, 將命令分成幾種類型, 並且分別調用到不同的接口來處理會顯得更加清晰一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章