前面我们了解过了当 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 协议 https://redis.io/topics/protocol 将命令进行编码.
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
- 845 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
- $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
- 1379 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.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
- 912 out = cliFormatReplyTTY(reply,"");
- (gdb) n
- 918 fwrite(out,sdslen(out),1,stdout);
- (gdb) p out
- $5 = (sds) 0x6949b3 "\"zhe\"\n"
最后将结果输出.
- More Redis internals: Tracing a GET & SET
- GDB cheatsheet https://darkdust.net/files/GDB Cheat Sheet.pdf
来源: https://juejin.im/post/5c45d0b96fb9a04a0d572bfe