1. 程式人生 > >redis 命令的調用過程

redis 命令的調用過程

以及 action href represent struct contents argv isp cut

參考文獻:

  1. Redis 是如何處理命令的(客戶端)
  2. 我是如何通過添加一條命令學習redis源碼的
  3. 從零開始寫redis客戶端(deerlet-redis-client)之路——第一個糾結很久的問題,restore引發的血案
  4. redis命令執行流程分析
  5. 通信協議(protocol)
  6. Redis主從復制原理
  7. Redis配置文件詳解

當用戶在redis客戶端鍵入一個命令的時候,客戶端會將這個命令發送到服務端。服務端會完成一系列的操作。一個redis命令在服務端大體經歷了以下的幾個階段:

  1. 讀取命令請求
  2. 查找命令的實現
  3. 執行預備操作
  4. 調用命令實現函數
  5. 執行後續工作

讀取命令的請求

從redis客戶端發送過來的命令,都會在readQueryFromClient函數中被讀取。當客戶端和服務器的連接套接字變的可讀的時候,就會觸發redis的文件事件。在aeMain函數中,將調用readQueryFromClient函數。在readQueryFromClient函數中,需要完成了2件事情:

  1. 將命令的內容讀取到redis客戶端數據結構中的查詢緩沖區。
  2. 調用processInputBuffer函數,根據協議格式,得出命令的參數等信息。
    例如命令 set key value 在query_buffer中將會以如下的格式存在:

技術分享圖片

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;

    ........ 
    ........
    
    // 讀入內容到查詢緩存
    nread = read(fd, c->querybuf+qblen, readlen);

    ........
    ........
    
    processInputBuffer(c);
}

命令參數的解析

在上一節中,我們看到在readQueryFromClient函數中會將套接字中的數據讀取到redisClient的queryBuf中。而對於命令的處理,實際是在processInputBuffer函數中進行的。
在函數中主要做了以下的2個工作:

  1. 判斷請求的類型,例如是內聯查詢還是多條查詢。具體的區別可以在通信協議(protocol)裏面看到。本文就不詳細敘述了。
  2. 根據請求的類型,調用不同的處理函數:
    2.1 processInlineBuffer
    2.2 processMultibulkBuffer
// 處理客戶端輸入的命令內容
void processInputBuffer(redisClient *c) {
    while(sdslen(c->querybuf)) {

        .......
        .......

        /* Determine request type when unknown. */
        // 判斷請求的類型
        // 兩種類型的區別可以在 Redis 的通訊協議上查到:
        // http://redis.readthedocs.org/en/latest/topic/protocol.html
        // 簡單來說,多條查詢是一般客戶端發送來的,
        // 而內聯查詢則是 TELNET 發送來的
        if (!c->reqtype) {
            if (c->querybuf[0] == ‘*‘) {
                // 多條查詢
                c->reqtype = REDIS_REQ_MULTIBULK;
            } else {
                // 內聯查詢
                c->reqtype = REDIS_REQ_INLINE;
            }
        }

        // 將緩沖區中的內容轉換成命令,以及命令參數
        if (c->reqtype == REDIS_REQ_INLINE) {
            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);
        }
    }
}

processMultibulkBuffer 和 processInlineBuffer

processMultibulkBuffer主要完成的工作是將 c->querybuf 中的協議內容轉換成 c->argv 中的參數對象。 比如 *3\r\n$3\r\nSET\r\n$3\r\nMSG\r\n$5\r\nHELLO\r\n將被轉換為:

 argv[0] = SET
 argv[1] = MSG
 argv[2] = HELLO

具體的過程就不貼代碼了。同樣processInlineBuffer也會完成將c->querybuf 中的協議內容轉換成 c->argv 中的參數的工作。

查找命令的實現

到了這一步,準備工作都做完了。redis服務器已將查詢緩沖中的命令轉換為參數對象了。接下來將調用processCommand函數進行命令的處理。processCommand函數比較長,接下來我們分段進行解析。

查找命令

服務器端首先開始查找命令。主要就是使用lookupCommand函數,根據命令對應的名字,去找到對應的執行函數以及相關的屬性信息。

    // 特別處理 quit 命令
    if (!strcasecmp(c->argv[0]->ptr,"quit")) {
        addReply(c,shared.ok);
        c->flags |= REDIS_CLOSE_AFTER_REPLY;
        return REDIS_ERR;
    }

    /* 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);
    if (!c->cmd) {
        // 沒找到指定的命令
        flagTransaction(c);
        addReplyErrorFormat(c,"unknown command ‘%s‘",
            (char*)c->argv[0]->ptr);
        return REDIS_OK;
    } else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
               (c->argc < -c->cmd->arity)) {
        // 參數個數錯誤
        flagTransaction(c);
        addReplyErrorFormat(c,"wrong number of arguments for ‘%s‘ command",
            c->cmd->name);
        return REDIS_OK;
    }

那麽命令的定義在哪裏呢?答案在redis.c文件中,定義了一個如下的實現:

struct redisCommand redisCommandTable[]= {
    .....
    
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    
    .....
}

Redis將所有它能支持的命令以及對應的“命令處理函數”之間對應關系存放在數組redisCommandTable[]中,該數組中保存元素的類型為結構體redisCommand,此中包括命令的名字以及對應處理函數的地址,在Redis服務初始化的時候,這個結構體會在初始化函數中被轉換成struct redisServer結構體中的一個dict,這個dict被賦值到commands域中。結構體詳細的實現如下:

/*
 * 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;
}

根據這個結構體,我們可以看到set執行的信息如下:

  1. 命令名稱是set
  2. 執行函數是setCommand
  3. 參數個數是3

執行命令前的準備工作

在上節,我們看到了Redis是如何查找命令,以及一個命令最終的定義和實現是在哪裏的。接下來我們來看下 processCommand後面部分的實現。這部分主要的工作是在執行命令之前做一點的檢查工作 :

  1. 檢查認證信息,如果redis服務器配置有密碼,在此處會做一次驗證
  2. 集群模式下的處理,此處不多做展開。
  3. 檢查是否到了Redis配置文件中,限制的最大內存數。如果達到了限制,需要根據配置的內存釋放策略做一定的釋放操作。
  4. 檢查是否主服務,並且這個服務器之前是否執行 BGSAVE 時發生了錯誤,如果發生了錯誤則不執行。
  5. 如果Redis服務器打開了min-slaves-to-write配置,則沒有足夠多的slave可寫的時候,拒絕執行寫操作。
  6. 如果當前的Redis服務器是個只讀的slave的話,拒絕執行寫操作。
  7. 當redis處於發布和訂閱上下文的時候,只能執行訂閱和退訂相關的命令。
  8. 如果slave-serve-stale-data 配置為no的時候,只允許INFO 和 SLAVEOF 命令。( Redis配置文件詳解)
  9. 如果服務器正在載入數據到數據庫,那麽只執行帶有 REDIS_CMD_LOADING 標識的命令,否則將出錯。
  10. 如果Lua 腳本超時,只允許執行限定的操作,比如 SHUTDOWN 和 SCRIPT KILL。

到此Redis執行一個命令前的檢查工作基本算完成了。接下來將調用call函數執行命令。

調用命令實現函數

在call函數裏面,在真正的執行一個命令的實現函數。

// 執行實現函數
c->cmd->proc(c);

那麽這個c是指什麽呢?我們來看下call函數的定義:

void call(redisClient *c, int flags) 

可見call函數傳入的是redisClient這個結構體的指針。那麽這個結構體在哪裏創建的呢?是在"讀取命令的請求"的階段就已經創建好了。在redisClient中,定義了一個struct redisCommand *cmd 屬性,在查找命令的階段便被賦予了對應命令的執行函數。因此在此處,將會調用對應的函數完成命令的執行。

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

執行後續工作

在執行完命令的實現函數之後,Redis還有做一些後續工作包括:

  1. 計算命令的執行時間
  2. 計算命令執行之後的 dirty 值
  3. 是否需要將命令記錄到SLOWLOG中
  4. 命令復制到 AOF 和 slave 節點

redis 命令的調用過程