上次说到了 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
函数中最核心的一步就是调用了 cmd
的 proc
方法,这个 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
,客户端即可收到返回消息。