走近原始碼:Redis命令執行過程(客戶端)

面向Google程式設計發表於2019-01-21

前面我們瞭解過了當Redis執行一個命令時,服務端做了哪些事情,不瞭解的同學可以看一下這篇文章走近原始碼:Redis如何執行命令。今天就一起來看看Redis的命令執行過程中客戶端都做了什麼事情。

啟動客戶端

首先看redis-cli.c檔案的main函式,也就是我們輸入redis-cli命令時所要執行的函式。main函式主要是給config變數的各個屬性設定預設值。比如:

  • hostip:要連線的服務端的IP,預設為127.0.0.1
  • hostport:要連線的服務端的埠,預設為6379
  • interactive:是否是互動模式,預設為0(非互動模式)
  • 一些模式的設定,例如:cluster_mode、slave_mode、getrdb_mode、scan_mode等
  • cluster相關的引數

……

接著呼叫parseOptions()函式來處理引數,例如-p、-c、–verbose等一些用來指定config屬性的(可以輸入redis-cli –help檢視)或是指定啟動模式的。

處理完這些引數後,需要把它們從引數列表中去除,剩下用於在非互動模式中執行的命令。

parseEnv()用來判斷是否需要驗證許可權,緊接著就是根據剛才的引數判斷需要進入哪種模式,是cluster還是slave又或者是RDB……如果沒有進入這些模式,並且沒有需要執行的命令,那麼就進入互動模式,否則會進入非互動模式。

/* Start interactive mode when no command is provided */if (argc == 0 &
&
!config.eval) {
/* Ignore SIGPIPE in interactive mode to force a reconnect */ signal(SIGPIPE, SIG_IGN);
/* Note that in repl mode we don't abort on connection error. * A new attempt will be performed for every command send. */ cliConnect(0);
repl();

}/* Otherwise, we have some arguments to execute */if (cliConnect(0) != REDIS_OK) exit(1);
if (config.eval) {
return evalMode(argc,argv);

} else {
return noninteractive(argc,convertToSds(argc,argv));

}複製程式碼

連線伺服器

cliConnect()函式用於連線伺服器,它的引數是一個標誌位,如果是CC_FORCE(0)表示強制重連,如果是CC_QUIET(2)表示不列印錯誤日誌。

如果建立了socket,那麼就連線這個socket,否則就去連線指定的IP和埠。

if (config.hostsocket == NULL) { 
context = redisConnect(config.hostip,config.hostport);

} else {
context = redisConnectUnix(config.hostsocket);

}複製程式碼
redisConnect

redisConnect()(在deps/hiredis/hiredis.c檔案中)函式用於連線指定的IP和埠的redis例項。它的返回值是redisContext型別的。這個結構封裝了一些客戶端與服務端之間的連線狀態,obuf是用來存放返回結果的緩衝區,同時還有客戶端與服務端的協議。

//hiredis.h/* Context for a connection to Redis */typedef struct redisContext {    int err;
/* Error flags, 0 when there is no error */ char errstr[128];
/* String representation of error when applicable */ int fd;
int flags;
char *obuf;
/* Write buffer */ redisReader *reader;
/* Protocol reader */ enum redisConnectionType connection_type;
struct timeval *timeout;
struct { char *host;
char *source_addr;
int port;

} tcp;
struct { char *path;

} unix_sock;

} redisContext;
複製程式碼

redisConnect的實現比較簡單,首先初始化一個redisContext變數,然後把客戶端的flags欄位設定為阻塞狀態,接著呼叫redisContextConnectTcp命令。

redisContext *redisConnect(const char *ip, int port) { 
redisContext *c;
c = redisContextInit();
if (c == NULL) return NULL;
c->
flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,NULL);
return c;

}複製程式碼
redisContextConnectTcp

redisContextConnectTcp()函式在net.c檔案中,它呼叫的是_redisContextConnectTcp()這個函式,所以我們主要關注這個函式。它用來與服務端建立TCP連線,首先調整了tcp的host和timeout欄位,然後getaddrinfo獲取要連線的服務資訊,這裡相容了IPv6和IPv4。然後嘗試連線服務端。

if (connect(s,p->
ai_addr,p->
ai_addrlen) == -1) {
if (errno == EHOSTUNREACH) {
redisContextCloseFd(c);
continue;

} else if (errno == EINPROGRESS &
&
!blocking) {
/* This is ok. */
} else if (errno == EADDRNOTAVAIL &
&
reuseaddr) {
if (++reuses >
= REDIS_CONNECT_RETRIES) {
goto error;

} else {
redisContextCloseFd(c);
goto addrretry;

}
} else {
if (redisContextWaitReady(c,timeout_msec) != REDIS_OK) goto error;

}
}複製程式碼

connect()函式用於去連線伺服器,連線上之後,伺服器端會呼叫accept函式。如果連線失敗,也會根據情況決定是否要關閉redisContext檔案描述符。

傳送命令並接收返回

當客戶端和服務端建立連線之後,客戶端向伺服器端傳送命令並接收返回值了。

repl

我們回到redis-cli.c檔案中的repl()函式,這個函式就是用來向伺服器端傳送命令並且接收到的結果返回。

這裡首先呼叫了cliInitHelp()和cliIntegrateHelp()這兩個函式,初始化了一些幫助資訊,然後設定了一些回撥的方法。如果是終端模式,則會從rc檔案中載入歷史命令。然後呼叫linenoise()函式讀取使用者輸入的命令,並以空格分隔引數。

nread = read(l.ifd,&
c,1);
複製程式碼

接下來是判斷是否需要過濾掉重複的引數。

issueCommandRepeat

生成好命令後,就呼叫issueCommandRepeat()函式開始執行命令。

static int issueCommandRepeat(int argc, char **argv, long repeat) { 
while (1) {
config.cluster_reissue_command = 0;
if (cliSendCommand(argc,argv,repeat) != REDIS_OK) {
cliConnect(CC_FORCE);
/* If we still cannot send the command print error. * We'll try to reconnect the next time. */ if (cliSendCommand(argc,argv,repeat) != REDIS_OK) {
cliPrintContextError();
return REDIS_ERR;

}
} /* Issue the command again if we got redirected in cluster mode */ if (config.cluster_mode &
&
config.cluster_reissue_command) {
cliConnect(CC_FORCE);

} else {
break;

}
} return REDIS_OK;

}複製程式碼

這個函式會呼叫cliSendCommand()函式,將命令傳送給伺服器端,如果傳送失敗,會強制重連一次,然後再次傳送命令。

redisAppendCommandArgv

cliSendCommand()函式又會呼叫redisAppendCommandArgv()函式(在hiredis.c檔案中)這個函式是按照Redis協議將命令進行編碼。

cliReadReply

然後呼叫cliReadReply()函式,接收伺服器端返回的結果,呼叫cliFormatReplyRaw()函式將結果進行編碼並返回。

舉個例子

我們以GET命令為例,具體描述一下,從客戶端到服務端,程式是如何執行的。

我們用gdb除錯redis-server,將斷點設定到readQueryFromClient函式這裡。

gdb src/redis-server GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-gitCopyright (C) 2018 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <
http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<
http://www.gnu.org/software/gdb/bugs/>
.Find the GDB manual and other documentation resources online at:<
http://www.gnu.org/software/gdb/documentation/>
.For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from src/redis-server...done.(gdb) b readQueryFromClientBreakpoint 1 at 0x43c520: file networking.c, line 1379.(gdb) run redis.conf複製程式碼

然後再除錯redis-cli,斷點設定cliReadReply函式。

gdb src/redis-cli GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-gitCopyright (C) 2018 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <
http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<
http://www.gnu.org/software/gdb/bugs/>
.Find the GDB manual and other documentation resources online at:<
http://www.gnu.org/software/gdb/documentation/>
.For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from src/redis-cli...done.(gdb) b cliReadReplyBreakpoint 1 at 0x40ffa0: file redis-cli.c, line 845.(gdb) run複製程式碼

在客戶端輸入get命令,發現程式在斷點處停止。

127.0.0.1:6379>
get jackeyBreakpoint 1, cliReadReply (output_raw_strings=output_raw_strings@entry=0) at redis-cli.c:845845 static int cliReadReply(int output_raw_strings) {複製程式碼

我們可以看到這時Redis已經準備好將命令傳送給服務端了,先來檢視一下要傳送的內容。

(gdb) p context->
obuf$1 = 0x684963 "*2\r\n$3\r\nget\r\n$6\r\njackey\r\n"複製程式碼

把\r\n替換成換行符看的後是這樣:

*2$3get$6jackey複製程式碼

*2表示命令引數的總數,包括命令的名字,也就是告訴服務端應該處理兩個引數。

$3表示第一個引數的長度。

get是命令名,也就是第一個引數。

$6表示第二個引數的長度。

jackey是第二個引數。

當程式執行到redisGetReply時就會把命令傳送給服務端了,這時我們再來看服務端的執行情況。

Thread 1 "redis-server" hit Breakpoint 1, readQueryFromClient (    el=0x7ffff6a41050, fd=7, privdata=0x7ffff6b1e340, mask=1)    at networking.c:13791379 void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {(gdb) 複製程式碼

程式調整到

sdsIncrLen(c->
querybuf,nread);
複製程式碼

這時nread的內容會被加到c->
querybuf中,我們來看一下是不是我們傳送過來的命令。

(gdb) p c->
querybuf$1 = (sds) 0x7ffff6a75cc5 "*2\r\n$3\r\nget\r\n$6\r\njackey\r\n"複製程式碼

到這裡,Redis的服務端已經接受到請求了。接下來就是處理命令的過程,前文我們提到Redis是在processCommand()函式中處理的。

processCommand()函式會呼叫lookupCommand()函式,從redisCommandTable表中查詢出要執行的函式。然後呼叫c->
cmd->
proc(c)執行這個函式,這裡我們get命令對應的是getCommand函式,getCommand裡只是呼叫了getGenericCommand()函式。

//t_string.cint getGenericCommand(client *c) { 
robj *o;
if ((o = lookupKeyReadOrReply(c,c->
argv[1],shared.null[c->
resp])) == NULL) return C_OK;
if (o->
type != OBJ_STRING) {
addReply(c,shared.wrongtypeerr);
return C_ERR;

} else {
addReplyBulk(c,o);
return C_OK;

}
}複製程式碼

lookupKeyReadOrReply()用來查詢指定key儲存的內容。並返回一個Redis物件,它的實現在db.c檔案中。

robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) { 
robj *o = lookupKeyRead(c->
db, key);
if (!o) addReply(c,reply);
return o;

}複製程式碼

在lookupKeyReadWithFlags函式中,會先判斷這個key是否過期,如果沒有過期,則會繼續呼叫lookupKey()函式進行查詢。

robj *lookupKey(redisDb *db, robj *key, int flags) { 
dictEntry *de = dictFind(db->
dict,key->
ptr);
if (de) {
robj *val = dictGetVal(de);
/* Update the access time for the ageing algorithm. * Don't do it if we have a saving child, as this will trigger * a copy on write madness. */ if (server.rdb_child_pid == -1 &
&
server.aof_child_pid == -1 &
&
!(flags &
LOOKUP_NOTOUCH)) {
if (server.maxmemory_policy &
MAXMEMORY_FLAG_LFU) {
updateLFU(val);

} else {
val->
lru = LRU_CLOCK();

}
} return val;

} else {
return NULL;

}
}複製程式碼

在這個函式中,先呼叫了dictFind函式,找到key對應的entry,然後再從entry中取出val。

找到val後,我們回到getGenericCommand函式中,它會呼叫addReplyBulk函式,將返回值新增到client結構的buf欄位。

(gdb) p c->
buf$18 = "$3\r\nzhe\r\n\n$8\r\nflushall\r\n:-1\r\n", '\000' <
repeats 16354 times>
複製程式碼

到這裡,get命令的處理過程已經完結了,剩下的事情就是將結果返回給客戶端,並且等待下次命令。

客戶端收到返回值後,如果是控制檯輸出,則會呼叫cliFormatReplyTTY對結果進行解析

(gdb) n912                 out = cliFormatReplyTTY(reply,"");
(gdb) n918 fwrite(out,sdslen(out),1,stdout);
(gdb) p out$5 = (sds) 0x6949b3 "\"zhe\"\n"複製程式碼

最後將結果輸出。

推薦閱讀

走近原始碼:Redis如何執行命令

More Redis internals: Tracing a GET &
SET

GDB cheatsheet

來源:https://juejin.im/post/5c45d0b96fb9a04a0d572bfe#comment

相關文章