继续我们上一节的讨论。服务器启动了,客户端也发送命令了。接下来,就要到服务器“表演”的时刻了。
1 服务器处理
服务器读取到命令请求后,会进行一系列的处理。
1.1 读取命令请求
当客户端与服务器之间的套接字因客户端的写入变得可读时,服务器将调用命令请求处理器执行以下操作:
- 读取套接字中的命令请求,并将其保存到客户端状态的输入缓冲区。
- 对输入缓冲区的命令请求进行分析,提取出命令请求中包含的命令参数及参数个数,然后分别将参数和参数个数保存到客户端状态的 argv 属性和 argc 属性里。
- 调用命令执行器,执行客户端指定的命令。
上面的 SET
命令保存到客户端状态的输入缓存区之后,客户端状态如图 4。
之后,分析程序将对输入缓冲区中的协议进行分析,并将得出的结果保存的客户端的 argv 和 argc 属性中,如图 5 所示:
之后,服务器将通过调用命令执行器来完成执行命令的余下步骤。
1.2 查找命令实现
命令执行器要做的第一件事就是根据 argv[0] 参数,在命令表(commandtable)中查找参数所指定的命令,并将找到的命令保存到 cmd 属性中。
命令表是一个字典,字典的键是一个个命令名称,比如 "SET"、"GET" 等。而字典的值则是一个个 redisCommand 结构,每个 redisCommand 结构记录了 Redis 命令的实现信息。源码如下:
# server.h/redisCommand
struct redisCommand {
char *name; // 命令名称。如 "SET"
redisCommandProc *proc; // 对应函数指针,指向命令的实现函数。比如 SET 对应的 setCommand 函数
int arity; // 命令参数的格个数。用来检查命令请求的格式是否合法。
// 要注意的命令的名称也是一个参数。像我们上面的 SET KEY VALUE 命令,实际上有三个参数。
char *sflags; // 字符串形式的标识值。记录了命令的属性。
int flags; // 对 sflags 标识分析得出的二进制标识,由程序自动生成。检查命令时,实际上使用的是此字段
redisGetKeysProc *getkeys_proc; // 指针函数,通过此方法来指定 key 的位置。
int firstkey; // 第一个 key 的位置
int lastkey; // 最后一个 key 的位置
int keystep; // key 之间的间距
long long microseconds, calls; // 命令的总调用时间及调用次数
};
另外,对于 sflags 属性,可使用的标识值及含义如下表:
标识 | 意义 | 带有此标识的命令 |
---|---|---|
w | 这是一个写入命令,可能会修改数据库 | SET、RPUSH、DEL 等 |
r | 这是一个只读命令,不会修改数据库 | GET、STRLEN 等 |
m | 此命令可能会占用大量内存,执行器需先检查内存使用情况,如果内存紧缺就禁止执行此命令 | SET、APPEND、RPUSH、SADD 等 |
a | 这是一个管理命令 | SAVE、BGSAVE 等 |
p | 这是一个发布与订阅功能的命令 | PUBLISH、SUBSRIBE 等 |
s | 这个命令不可以在 lua 脚步中使用 | BPOP、BLPOP 等 |
R | 这是一个随机命令。对于相同的数据集和相同的参数,返回结果可能不同 | SPOP、SRANDMEMBER 等 |
S | 当在 lua 脚步中使用此命令时,对返回结果进行排序,使得结果有序 | SINTER、SUNION 等 |
l | 这个命令可以在服务器载入数据的过程中使用 | INFO、PUBLISH 等 |
t | 这个命令允许在从库有过期数据时使用 | SLAVEOF、PING 等 |
M | 这个命令在监视模式下,不会被自动传播 | EXEC |
k | 集群模式下,如果对应槽点标记位“导入”,则接受此命令 | restore-asking |
F | 这个命令在程序执行时应该立刻执行 | SETNX、GET 等 |
命令表结构如图 6:
对于我们上面的 SET KEY VALUE
命令,当程序以图 5 中的 argv[0] 作为输入,在命令表中进行查找时,命令表返回 "set" 键对于的 redisCommand 结构,客户端状态的 cmd 指针会指向这个 redisCommand 结构。如图 7 所示:
要注意的是,对于 Redis 而言,命令名字的大小写不影响命令表的查找结果,也就是命令名称不区分大小写。执行 SET 和 set、Set 将获得相同结果。
1.3 执行预备操作
到目前为止,服务器已经将执行命令所需要的命令实现函数(客户端 cmd 属性)、参数(客户端 argv 属性)、参数个数(客户端 argc 属性)都初始化完毕。但在真正执行命令之前,程序还会进行一些预备操作,保证命令可以正确、顺利的被执行。预备操作包括:
- 检查客户端的 cmd 指针是否指向 NULL,如果是的话,说明用户输入的命令名称没有对应的函数,服务器将不再执行后续操作,并向客户端返回一个错误。
- 根据客户端 cmd 属性指向的 redisCommand 结果的 arity 属性,检查命令请求所给定的参数个数是否正确。
- 检查客户端是否已经通过了身份验证。未通过身份验证的客户端只能执行
AUTH
命令。否则,将会向客户端返回一个错误。 - 如果服务器打开了 maxmemory 功能,在执行命令之前,会先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,将不再执行后续步骤,向客户端返回一个错误。
- 如果服务器上一次执行
BGSAVE
命令时出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,而将要执行的命令是一个写命令,那么服务器将拒绝执行这个鞋命令,并向客户端返回一个错误。 - 如果客户端正在用
SUBSCRIBE
和PSUBSCRIBE
命令订阅频道或模式,那么服务器只会执行客户端发来的SUBSCRIBE
、PSUBSCRIBE
、UNSUBSCRIBE
、PUNSUBSCRIBE
四个命令,其它命令都会被拒绝。 - 如果服务器正在进行数据载入,那么客户端发送是命令必须带有
l
标识才会被服务器执行。 - 如果客户端正在执行事务,那么服务器只会执行
EXEC
、DISCARD
、MULTI
、WATCH
四个命令,其他命令都会被放进事务队列中。 - 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。
当完成了以上预备操作之后,服务器就开始真正的执行命令了。
要注意的是,上面列出的预备操作只是服务器在单机模式下的检查操作。如果在复制或者集群模式下,预备操作还会更多。
1.4 调用命令的实现函数
在前面的操作中 ,服务器已经将要执行的命令实现、参数、参数个数保存在客户端结构中。
对于我们上面的 SET KEY VALUE
命令,图 8 包含了命令实现、参数和参数个数结构:
当服务器决定要执行命令时,只要执行以下语句即可:
// client 是指向客户端状态的指针。server.c/call()
client->cmd->proc(client);
上面的执行语句实际上就是调用 setCommand
函数(t_string.c)。
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区中(bug 属性 和 reply 属性),之后实现函数会为客户端的套接字关联命令回复处理器,由命令回复处理器返回给客户端。
回到我们的示例,setCommand(client)
将产生一个 "+OK
" 回复,这个回复被保存在客户端的 buf 属性中。如图 9 所示:
1.5 执行后续工作
实现函数执行完后,服务器还会执行一些后续工作,主要包括:
- 如果服务器开启了 slow-log 功能,那么慢查询日志模块将会检查是否需要将刚执行的命令添加到慢查询日志。
- 更新
redisCommand
结构的 milliseconds 和 calls 属性。 - 如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区中。
- 如果有其它服务器正在复制当前这个服务器,那么服务器将会把刚刚执行的命令传播给所有从服务器。
以上后续操作执行完毕后,一条执行命令也就执行完成了。服务器可以继续处理后续的命令。
1.6 将命令回复发送给客户端
上面过程中,命令实现函数会将命令回复保存到客户端的输出缓冲区中,并为客户端的套接字关联命令回复处理器。当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将命令回复发送给客户端。
当命令回复发送完毕后,回复处理器会情况客户端的输出缓冲区,为处理下一个命令请求做好准备。
以图 9 所示的客户端状态为例,当客户端的套接字变为可写状态时,命令回复处理器会将协议格式的命令回复 "+OK " 发送给客户端。
1.7 源码解读
命令处理请求,函数调用堆栈信息如图 3-7-1:
命令回复,函数调用堆栈信息如图 3-7-2:
2 客户端接收并打印回复
客户端接收到命令回复之后,会将回复转换成我们可读的格式,并打印在屏幕上(对于 redis-cli 客户端),如图 10 所示。
至此,我们走完了从发起一个命令请求,到收到回复的所有过程。对于我们最开始提的问题,服务器如何响应客户端请求,你有答案了吗?
总结
- 服务器通过
networking.c/readQueryFromClient()
读取和执行对应命令。 - 服务器通过
networking.c/writeToClient()
将命令回复发送给客户端。