Redis命令的請求和執行過程

一、創建客戶端

當Redis服務器和客戶端建立連接之後,會創建一個客戶端,這是因爲Redis是I/O多路複用,所以服務器需要維持爲每一個客戶端維持一個狀態。創建好了之後會綁定讀事件到EventLoop。這個時候當客戶端有讀事件發生時,服務器就可以讀取客戶端的數據進行下一步處理。

/*
 * 創建一個新客戶端
 */
redisClient *createClient(int fd) {

    // 分配空間
    redisClient *c = zmalloc(sizeof(redisClient));

    /* passing -1 as fd it is possible to create a non connected client.
     * This is useful since all the Redis commands needs to be executed
     * in the context of a client. When commands are executed in other
     * contexts (for instance a Lua script) we need a non connected client. */
    // 當 fd 不爲 -1 時,創建帶網絡連接的客戶端
    // 如果 fd 爲 -1 ,那麼創建無網絡連接的僞客戶端
    // 因爲 Redis 的命令必須在客戶端的上下文中使用,所以在執行 Lua 環境中的命令時
    // 需要用到這種僞終端
    if (fd != -1) {
        // 非阻塞
        anetNonBlock(NULL,fd);
        // 禁用 Nagle 算法
        anetEnableTcpNoDelay(NULL,fd);
        // 設置 keep alive
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);
        // 綁定讀事件到事件 loop (開始接收命令請求)
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
//這個函數後邊的代碼省略.......
return c;
}

二、讀取客戶端數據

當有讀事件發生時,會調用readQueryFromClient函數,這個函數會讀取連接描述符的數據,存到客戶端的queryBuf中。並且調用processInputBuffer函數來從查詢緩衝區中讀取內容,創建參數,並執行命令。

/*
 * 讀取客戶端的查詢緩衝區內容
 */
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    int nread, readlen;
    size_t qblen;
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);

    // 設置服務器的當前客戶端
    server.current_client = c;

    // 讀入長度(默認爲 16 MB)
    readlen = REDIS_IOBUF_LEN;

    /* If this is a multi bulk request, and we are processing a bulk reply
     * that is large enough, try to maximize the probability that the query
     * buffer contains exactly the SDS string representing the object, even
     * at the risk of requiring more read(2) calls. This way the function
     * processMultiBulkBuffer() can avoid copying buffers to create the
     * Redis Object representing the argument. */
    if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= REDIS_MBULK_BIG_ARG)
    {
        int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);

        if (remaining < readlen) readlen = remaining;
    }

    // 獲取查詢緩衝區當前內容的長度
    // 如果讀取出現 short read ,那麼可能會有內容滯留在讀取緩衝區裏面
    // 這些滯留內容也許不能完整構成一個符合協議的命令,
    qblen = sdslen(c->querybuf);
    // 如果有需要,更新緩衝區內容長度的峯值(peak)
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    // 爲查詢緩衝區分配空間
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    // 讀入內容到查詢緩存
    nread = read(fd, c->querybuf+qblen, readlen);

    // 讀入出錯
    if (nread == -1) {
        if (errno == EAGAIN) {
            nread = 0;
        } else {
            redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    // 遇到 EOF
    } else if (nread == 0) {
        redisLog(REDIS_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }

    if (nread) {
        // 根據內容,更新查詢緩衝區(SDS) free 和 len 屬性
        // 並將 '\0' 正確地放到內容的最後
        sdsIncrLen(c->querybuf,nread);
        // 記錄服務器和客戶端最後一次互動的時間
        c->lastinteraction = server.unixtime;
        // 如果客戶端是 master 的話,更新它的複製偏移量
        if (c->flags & REDIS_MASTER) c->reploff += nread;
    } else {
        // 在 nread == -1 且 errno == EAGAIN 時運行
        server.current_client = NULL;
        return;
    }

    // 查詢緩衝區長度超出服務器最大緩衝區長度
    // 清空緩衝區並釋放客戶端
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
        sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();

        bytes = sdscatrepr(bytes,c->querybuf,64);
        redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }

    // 從查詢緩存重讀取內容,創建參數,並執行命令
    // 函數會執行到緩存中的所有內容都被處理完爲止
    processInputBuffer(c);

    server.current_client = NULL;
}

三、讀取緩衝區內容,創建參數列表

processInputBuffer函數。這個函數調用了有三個關鍵的函數,一個是processInlineBuffer,一個是processMultibulkBuffer, 還有一個是processCommand函數。redis支持兩種協議,一種是inline ,另一種是multibulk協議。inline協議是老協議,這裏不做討論。現在主要是multibulk協議,這個協議把客戶端輸入的命令,比如“set mykey myvalue”轉化成“3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n”其中“”之後的“3”表示總共有三個參數。轉化成協議格式後傳輸到服務器。服務器要解析這串字符成爲可執行的命令。
processInlineBuffer和processmultibulk這兩個函數把客戶端發送來的協議解析成argc(參數總數)和argv(參數列表)。保存在redisClient中。
這裏有必要再看看客戶端結構體中相關的成員定義:

/* With multiplexing we need to take per-client state.
 * Clients are taken in a liked list.
 *
 * 因爲 I/O 複用的緣故,需要爲每個客戶端維持一個狀態。
 *
 * 多個客戶端狀態被服務器用鏈表連接起來。
 */
typedef struct redisClient {

    // 套接字描述符
    int fd;

    // 當前正在使用的數據庫
    redisDb *db;

    // 當前正在使用的數據庫的 id (號碼)
    int dictid;

    // 客戶端的名字
    robj *name;             /* As set by CLIENT SETNAME */

    // 查詢緩衝區
    // 注意,sds就是char*
    sds querybuf;

    // 查詢緩衝區長度峯值
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */

    // 參數數量
    int argc;

    // 參數對象數組
    robj **argv;

    // 記錄被客戶端執行的命令
    struct redisCommand *cmd, *lastcmd;

    // 請求的類型:內聯命令還是多條命令
    int reqtype;

//省略後邊的成員變量。。。。。
    } redisClient;

也就是說這兩個函數把*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n”中的3(3個參數)賦值給argc, SET, mykey, myvalue賦值給argv.

processInputBuffer的源代碼

// 處理客戶端輸入的命令內容
void processInputBuffer(redisClient *c) {

    /* Keep processing while there is something in the input buffer */
    // 儘可能地處理查詢緩衝區中的內容
    // 如果讀取出現 short read ,那麼可能會有內容滯留在讀取緩衝區裏面
    // 這些滯留內容也許不能完整構成一個符合協議的命令,
    // 需要等待下次讀事件的就緒
    while(sdslen(c->querybuf)) {

        /* Return if clients are paused. */
        // 如果客戶端正處於暫停狀態,那麼直接返回
        if (!(c->flags & REDIS_SLAVE) && clientsArePaused()) return;

        /* Immediately abort if the client is in the middle of something. */
        // REDIS_BLOCKED 狀態表示客戶端正在被阻塞
        if (c->flags & REDIS_BLOCKED) return;

        /* REDIS_CLOSE_AFTER_REPLY closes the connection once the reply is
         * written to the client. Make sure to not let the reply grow after
         * this flag has been set (i.e. don't process more commands). */
        // 客戶端已經設置了關閉 FLAG ,沒有必要處理命令了
        if (c->flags & REDIS_CLOSE_AFTER_REPLY) return;

        /* Determine request type when unknown. */
        // 判斷請求的類型
        // 兩種類型的區別可以在 Redis 的通訊協議上查到:
        // http://redis.readthedocs.org/en/latest/topic/protocol.html
        // 簡單來說,多條查詢是一般客戶端發送來的,
        // 而內聯查詢則是 TELNET 發送來的

        // 如果querybuf[0]的第一個字符是*的話,就是multibulk,否則是inline
        // redis 支持兩種協議,一種是inline,一種是mutibulk協議,inline是老協議
        if (!c->reqtype) {
            if (c->querybuf[0] == '*') {
                // 多條查詢
                c->reqtype = REDIS_REQ_MULTIBULK;
            } else {
                // 內聯查詢
                c->reqtype = REDIS_REQ_INLINE;
            }
        }

        // 將緩衝區中的內容轉換成命令,以及命令參數
        if (c->reqtype == REDIS_REQ_INLINE) {
            //解析客戶端的單行請求,成功返回REDIS_OK
            if (processInlineBuffer(c) != REDIS_OK) break;
        } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
            //解析客戶端的多行請求
            if (processMultibulkBuffer(c) != REDIS_OK) break;
        } else {
            redisPanic("Unknown request type");
        }

        /* Multibulk processing could see a <= 0 length. */
        if (c->argc == 0) {
            resetClient(c);
        } else {
            /* Only reset the client when the command was executed. */
            // 執行命令,並重置客戶端
            if (processCommand(c) == REDIS_OK)
                resetClient(c);
        }
    }
}

四、執行命令

調用了一個函數,processCommand,執行命令。
這個函數先調用lookupCommand來查找命令,然後調用call函數來執行命令,最後調用addReply來回復給客戶端。

要理解這個過程,必須知道Redis中命令是怎麼存儲的。

redis定義了一個結構體來存儲一個命令的相關信息。比如命令名稱,命令的實現函數,命令的參數個數等等。

/*
 * Redis 命令
 */
struct redisCommand {

    // 命令名字
    char *name;

    // 實現函數
    redisCommandProc *proc;

    // 參數個數
    int arity;

    // 字符串表示的 FLAG
    char *sflags; /* Flags as string representation, one char per flag. */

    // 實際 FLAG
    int flags;    /* The actual flags, obtained from the 'sflags' field. */

    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    // 從命令中判斷命令的鍵參數。在 Redis 集羣轉向時使用。
    redisGetKeysProc *getkeys_proc;

    /* What keys should be loaded in background when calling this command? */
    // 指定哪些參數是 key
    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 */

    // 統計信息
    // microseconds 記錄了命令執行耗費的總毫微秒數
    // calls 是命令被執行的總次數
    long long microseconds, calls;
};

然後在struct redisServer中定義了一個dict字典類型的命令表。其中commands是要通過rename配置選項配置,而orig_commands
是原始的命令表。不受redis.conf的影響。

struct redisServer {

      。。。。。。。

    // 命令表(受到 rename 配置選項的作用)
    dict *commands;             /* Command table */
    // 命令表(無 rename 配置選項的作用)
    dict *orig_commands;        /* Command table before command renaming. */

    。。。。。。。。。
    }

然後在redis.c 中定義了一個redisCommandTable的初始化表

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    ........,}

然後通過populateCommandTable函數將這個redisCommandTable中的redisCommand添加到redisServer中的dict型orig_commands中,每次查找就可以通過字典來查找相應的redisCommands,然後就可以調用它所對應的proc函數,來實現對命令的處理,最後通過addReply來回復客戶端的處理情況。

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