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