走近原始碼:Redis命令執行過程(客戶端)
前面我們瞭解過了當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-git Copyright (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 readQueryFromClient Breakpoint 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-git Copyright (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 cliReadReply Breakpoint 1 at 0x40ffa0: file redis-cli.c, line 845. (gdb) run 複製程式碼
在客戶端輸入get命令,發現程式在斷點處停止。
127.0.0.1:6379> get jackey Breakpoint 1, cliReadReply (output_raw_strings=output_raw_strings@entry=0) at redis-cli.c:845 845static 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 $3 get $6 jackey 複製程式碼
*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:1379 1379void 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.c int 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) n 912out = cliFormatReplyTTY(reply,""); (gdb) n 918fwrite(out,sdslen(out),1,stdout); (gdb) p out $5 = (sds) 0x6949b3 "\"zhe\"\n" 複製程式碼
最後將結果輸出。