Redis 的命令执行流程

  上次说到了 Redis 的网络模型和 IO 线程的设计,那这一篇书接上回,继续来讲一讲 Redis 是怎么处理一条来自客户端的命令的。

初始化所有命令

  Redis 现在已经有了不少的命令,所以有一个可方便扩展的命令生成规则就很重要了。Redis 目前是将所有命令都写在了 src/commands 中,每个命令都有自己一个专属的 json 文件来描述,下面这段代码来自 get.json, 它是 get 命令的描述。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
    "GET": {
        "summary": "Get the value of a key",
        "complexity": "O(1)",
        "group": "string",
        "since": "1.0.0",
        "arity": 2,
        "function": "getCommand",
        "command_flags": [
            "READONLY",
            "FAST"
        ],
        "acl_categories": [
            "STRING"
        ],
        "key_specs": [
            {
                "flags": [
                    "RO",
                    "ACCESS"
                ],
                "begin_search": {
                    "index": {
                        "pos": 1
                    }
                },
                "find_keys": {
                    "range": {
                        "lastkey": 0,
                        "step": 1,
                        "limit": 0
                    }
                }
            }
        ],
        "arguments": [
            {
                "name": "key",
                "type": "key",
                "key_spec_index": 0
            }
        ]
    }
}

  其中既有一些说明性质的字段,也有一些是与命令执行相关的字段,比如 function 字段的值就是本命令对应的处理函数,arguments 是命令需要的参数。
  所有的 json 文件会被 generate-command-code.py 这个 python 脚本处理并且生成最终的结果,src/commands.c,不过一般编译的时候并不会重新生成,因为 redis 的源码中已经自带了生成好的最终文件,这一点在 Makefile 中也有说明。

1
2
3
4
5
6
# The file commands.c is checked in and doesn't normally need to be rebuilt. It
# is built only if python is available and its prereqs are modified.
ifneq (,$(PYTHON))
commands.c: commands/*.json ../utils/generate-command-code.py
    $(QUIET_GEN)$(PYTHON) ../utils/generate-command-code.py
endif

  在 commands.c 文件中,最重要的部分就是一个 struct redisCommand 结构体数据的变量 redisCommandTable,它就是之前所有 json 文件生成的最终结果。每一个 json 文件都对应生成了一个 redisCommand 变量。由于生成的内容都在一行里,基本不具备可读性,这里就不贴代码了。
  redisCommand 这个结构体中基本就是保存了 json 文件中的内容,字段很多,但是基本不太需要注意,就不全贴上来了,只有一个变量需要特别注意的,就是 proc,它是本命令的执行方法,对应的就是 json 文件中的 function 字段的内容。
  服务器进程在启动阶段会调用 populateCommandTable 加载所有的命令,这个函数做的事情很简单,就是不断遍历 redisCommandTable 这个数组,加载命令到 server.commands 这个字典中,后续命令查找都是通过这个字典来进行查找的。

1
2
3
4
// 省略其它字段
struct redisServer {
    dict *commands; /* Command table */
}

读取命令

  在上篇中一直被提到,但是并不是重点的 readQueryFromClient 在本篇中要正式登场了,它比较长,但是处理的内容并不复杂,照惯例还是贴一个精简版的源码上来,只留下最基本的处理流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 精简版,仅留下最基本的流程
void readQueryFromClient(connection *conn) {
    client *c = connGetPrivateData(conn); // 根据 conn 拿到 client
    int nread, big_arg = 0;
    size_t qblen, readlen;
    readlen = PROTO_IOBUF_LEN; // 默认 16KB
    qblen = sdslen(c->querybuf); // 拿到当前已写入的长度
    nread = connRead(c->conn, c->querybuf + qblen, readlen); // 读取数据
    sdsIncrLen(c->querybuf, nread);
    qblen = sdslen(c->querybuf);
    if (processInputBuffer(c) == C_ERR) // 解析并执行 buffer 中的内容
         c = NULL;
}

  经过简化以后,可以看到 readQueryFromClient 本质上做的事情就是从 conn 中读取数据到 client 的 querybuf 中,然后调用 processInputBuffer 做后续处理。

执行命令

  读取过程结束以后,调用了 processInputBuffer 来负责后续过程,这个函数经过简化以后,只留下基本步骤,就是这样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int processInputBuffer(client *c) {
    while (c->qb_pos < sdslen(c->querybuf)) {

        // 解析 buf,解析结果放在 client 的 argc 和 argv 中,增加 qb_pos
        if (processInlineBuffer(c) != C_OK)
            break;
        
        // 执行命令
        if (processCommandAndResetClient(c) == C_ERR) {
            return C_ERR;
        }
    }
    if (c->qb_pos) {
        sdsrange(c->querybuf, c->qb_pos, -1); // 收缩 querybuf
        c->qb_pos = 0;
    }

    return C_OK;
}

  经过简化以后,代码的逻辑很简单,就是不断使用 processInlineBuffer 解析 client 中 querybuf 的数据,然后调用 processCommandAndResetClient 处理解析后的结果。由于解析过程并不太重要,而且也不复杂,所以这里就不再讨论了,后面只看处理过程。
  由于 processCommandAndResetClient 只是对 processCommand 的简单调用,所以我们直接来看 processCommand 的操作,这是个非常长的函数,算上注释有三百多行,但是不要怕,还是只看最基本的执行流程,简化过以后如下。

1
2
3
4
5
6
int processCommand(client *c) {
    // 根据 client 的 argc 和 argv 在命令表中查找命令
    c->cmd = c->lastcmd = c->realcmd = lookupCommand(c->argv, c->argc);
    call(c, CMD_CALL_FULL); // 最后执行命令
    return C_OK;
}

  排除掉所有异常状态判断,权限验证,内存容量检查等等操作以后,其实 processCommand 就只是做了两件事,先在命令表中查找命令,然后使用 call 执行了命令。
  call 又是一个算注释二百行的函数,其中依然有大量的统计类操作和异常处理操作,但是核心逻辑非常简单,照惯例贴上一个只保留基本流程的函数内容。

1
2
3
4
5
6
7
void call(client *c, int flags) {
    const long long call_timer = ustime(); // 起始时间打点
    c->cmd->proc(c); // 调用命令的 proc 函数
    ustime_t duration;
    duration = ustime() - call_timer;
    c->duration = duration; // 保存本条命令执行的时间
}

  在 call 函数中最核心的一步就是调用了 cmdproc 方法,这个 proc 就是最开始注册命令时的那个 function,还是以 get 为例,它对应的方法是 getCommand,看一下它的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int getGenericCommand(client *c) {
    robj *o;

    // 查找指定对象
    if ((o = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp])) == NULL)
        return C_OK;

    if (checkType(c, o, OBJ_STRING)) {
        return C_ERR;
    }

    addReplyBulk(c, o);
    return C_OK;
}

void getCommand(client *c) {
    getGenericCommand(c);
}

  它调用了 lookupKeyReadOrReply 查找客户端 argv 数组的第一个值,也就是 get 命令的参数,在这个函数中,会去 client 目前对应的 db 中去查找这个 key 对应的对象,查找的过程不在本篇的范围内,暂时按下不表,后面有机会再讨论。
  如果查找到了对应的对象,会调用 addReplyBulk 将要返回的对象复制到 client 的 buf 中,等待后续的处理。

返回命令结果

  结合上一篇讲网络模型的文章中有提到的,Redis 在写数据的步骤会为每个等待返回的客户端调用 writeToClient,这个函数中一样还是有很多处理异常和进行统计的部分,这些并不是本篇关注的重点,它实际上又调用了 _writeToClient 来发送数据,所以我们直接来看 _writeToClient 的实现即可,照例还是精简一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int _writeToClient(client *c, ssize_t *nwritten) {
    *nwritten = 0;
    if (c->bufpos > 0) {
        *nwritten = connWrite(c->conn, c->buf + c->sentlen, c->bufpos - c->sentlen);
        if (*nwritten <= 0)
            return C_ERR;
        c->sentlen += *nwritten;

        if ((int)c->sentlen == c->bufpos) {
            c->bufpos = 0;
            c->sentlen = 0;
        }
    }

    return C_OK;
}

  这里精简过以后就很简单了,就是使用了 connWrite 把 client 的 buf 中的数据发送给了 client 的 conn,客户端即可收到返回消息。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计