继续我们上一节的讨论。服务器启动了,客户端也发送命令了。接下来,就要到服务器“表演”的时刻了。
服务器读取到命令请求后,会进行一系列的处理。
当客户端与服务器之间的套接字因客户端的写入变得可读时,服务器将调用命令请求处理器执行以下操作:
上面的 SET
命令保存到客户端状态的输入缓存区之后,客户端状态如图 4。
之后,分析程序将对输入缓冲区中的协议进行分析,并将得出的结果保存的客户端的 argv 和 argc 属性中,如图 5 所示:
之后,服务器将通过调用命令执行器来完成执行命令的余下步骤。
命令执行器要做的第一件事就是根据 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 将获得相同结果。
到目前为止,服务器已经将执行命令所需要的命令实现函数(客户端 cmd 属性)、参数(客户端 argv 属性)、参数个数(客户端 argc 属性)都初始化完毕。但在真正执行命令之前,程序还会进行一些预备操作,保证命令可以正确、顺利的被执行。预备操作包括:
AUTH
命令。否则,将会向客户端返回一个错误。BGSAVE
命令时出错,并且服务器打开了 stop-writes-on-bgsave-error 功能,而将要执行的命令是一个写命令,那么服务器将拒绝执行这个鞋命令,并向客户端返回一个错误。SUBSCRIBE
和 PSUBSCRIBE
命令订阅频道或模式,那么服务器只会执行客户端发来的 SUBSCRIBE
、PSUBSCRIBE
、UNSUBSCRIBE
、PUNSUBSCRIBE
四个命令,其它命令都会被拒绝。l
标识才会被服务器执行。EXEC
、DISCARD
、MULTI
、WATCH
四个命令,其他命令都会被放进事务队列中。当完成了以上预备操作之后,服务器就开始真正的执行命令了。
要注意的是,上面列出的预备操作只是服务器在单机模式下的检查操作。如果在复制或者集群模式下,预备操作还会更多。
在前面的操作中 ,服务器已经将要执行的命令实现、参数、参数个数保存在客户端结构中。
对于我们上面的 SET KEY VALUE
命令,图 8 包含了命令实现、参数和参数个数结构:
当服务器决定要执行命令时,只要执行以下语句即可:
// client 是指向客户端状态的指针。server.c/call()
client->cmd->proc(client);
上面的执行语句实际上就是调用 setCommand
函数(t_string.c)。
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区中(bug 属性 和 reply 属性),之后实现函数会为客户端的套接字关联命令回复处理器,由命令回复处理器返回给客户端。
回到我们的示例,setCommand(client)
将产生一个 "+OK\r\n" 回复,这个回复被保存在客户端的 buf 属性中。如图 9 所示:
实现函数执行完后,服务器还会执行一些后续工作,主要包括:
redisCommand
结构的 milliseconds 和 calls 属性。以上后续操作执行完毕后,一条执行命令也就执行完成了。服务器可以继续处理后续的命令。
上面过程中,命令实现函数会将命令回复保存到客户端的输出缓冲区中,并为客户端的套接字关联命令回复处理器。当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将命令回复发送给客户端。
当命令回复发送完毕后,回复处理器会情况客户端的输出缓冲区,为处理下一个命令请求做好准备。
以图 9 所示的客户端状态为例,当客户端的套接字变为可写状态时,命令回复处理器会将协议格式的命令回复 "+OK\r\n" 发送给客户端。
命令处理请求,函数调用堆栈信息如图 3-7-1:
命令回复,函数调用堆栈信息如图 3-7-2:
客户端接收到命令回复之后,会将回复转换成我们可读的格式,并打印在屏幕上(对于 redis-cli 客户端),如图 10 所示。
至此,我们走完了从发起一个命令请求,到收到回复的所有过程。对于我们最开始提的问题,服务器如何响应客户端请求,你有答案了吗?
networking.c/readQueryFromClient()
读取和执行对应命令。networking.c/writeToClient()
将命令回复发送给客户端。