redis 命令的調用過程
參考文獻:
- Redis 是如何處理命令的(客戶端)
- 我是如何通過添加一條命令學習redis源碼的
- 從零開始寫redis客戶端(deerlet-redis-client)之路——第一個糾結很久的問題,restore引發的血案
- redis命令執行流程分析
- 通信協議(protocol)
- Redis主從復制原理
- Redis配置文件詳解
當用戶在redis客戶端鍵入一個命令的時候,客戶端會將這個命令發送到服務端。服務端會完成一系列的操作。一個redis命令在服務端大體經歷了以下的幾個階段:
- 讀取命令請求
- 查找命令的實現
- 執行預備操作
- 調用命令實現函數
- 執行後續工作
讀取命令的請求
從redis客戶端發送過來的命令,都會在readQueryFromClient函數中被讀取。當客戶端和服務器的連接套接字變的可讀的時候,就會觸發redis的文件事件。在aeMain函數中,將調用readQueryFromClient函數。在readQueryFromClient函數中,需要完成了2件事情:
- 將命令的內容讀取到redis客戶端數據結構中的查詢緩沖區。
- 調用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個工作:
- 判斷請求的類型,例如是內聯查詢還是多條查詢。具體的區別可以在通信協議(protocol)裏面看到。本文就不詳細敘述了。
- 根據請求的類型,調用不同的處理函數:
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執行的信息如下:
- 命令名稱是set
- 執行函數是setCommand
- 參數個數是3
執行命令前的準備工作
在上節,我們看到了Redis是如何查找命令,以及一個命令最終的定義和實現是在哪裏的。接下來我們來看下 processCommand後面部分的實現。這部分主要的工作是在執行命令之前做一點的檢查工作 :
- 檢查認證信息,如果redis服務器配置有密碼,在此處會做一次驗證
- 集群模式下的處理,此處不多做展開。
- 檢查是否到了Redis配置文件中,限制的最大內存數。如果達到了限制,需要根據配置的內存釋放策略做一定的釋放操作。
- 檢查是否主服務,並且這個服務器之前是否執行 BGSAVE 時發生了錯誤,如果發生了錯誤則不執行。
- 如果Redis服務器打開了min-slaves-to-write配置,則沒有足夠多的slave可寫的時候,拒絕執行寫操作。
- 如果當前的Redis服務器是個只讀的slave的話,拒絕執行寫操作。
- 當redis處於發布和訂閱上下文的時候,只能執行訂閱和退訂相關的命令。
- 如果slave-serve-stale-data 配置為no的時候,只允許INFO 和 SLAVEOF 命令。( Redis配置文件詳解)
- 如果服務器正在載入數據到數據庫,那麽只執行帶有 REDIS_CMD_LOADING 標識的命令,否則將出錯。
- 如果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還有做一些後續工作包括:
- 計算命令的執行時間
- 計算命令執行之後的 dirty 值
- 是否需要將命令記錄到SLOWLOG中
- 命令復制到 AOF 和 slave 節點
redis 命令的調用過程