參考文獻:
- 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 節點