走近源码:Redis命令执行过程(客户端)

英文解释
Redis: under the hood

启动客户端

首先看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
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"

最后将结果输出。

推荐阅读

走近源码:Redis如何执行命令

More Redis internals: Tracing a GET & SET

GDB cheatsheet

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342