一个命令请求从客户端发送到获得响应过程中,客户端和服务端要完成一系列操作。例如在客户端操作如下命令:
redis> set key value
1
- 客户端向服务端发送命令请求 set key value;
- 服务器接收并处理该命令请求,在数据库中进行设置操作,并产生命令回复 1;
- 服务器将命令回复发送给客户端;
- 客户端接收服务器返回信息,并将信息打印给用户。
一、发送命令请求
当用户在客户端键入命令后,客户端会先对命令转换协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。譬如输入如下命令:
set key value
那么客户端就会将其转化为如下格式:
*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
最终将这段内容发送给服务器。
二、读取命令请求
当客户端与服务端的连接套接字因为客户端的写入而变得可读时,服务端将调用命令请求处理器来进行一下操作:
- 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区 querybuf 中;
- 对输入缓冲区中的命令进行分析,提取出命令请求中包含的命令参数和参数个数,然后分别赋值给客户端状态的 argv 属性和 argc 属性中;
- 调用命令执行器,执行客户端指定的命令。
在介绍 redis之服务器初始化 时,提到过 struct redisServer 结构体还有这么个属性:
struct redisServer {
...
list *clients; /* List of active clients */
...
}
该属性就记录了连接当前服务器的各个客户端状态,而各个客户端实际上也有一个对应的结构体 struct client,部分属性如下:
typedef struct client {
...
redisDb *db; /* Pointer to currently SELECTed DB. */
robj *name; /* As set by CLIENT SETNAME. */
sds querybuf; /* Buffer we use to accumulate client queries. */
...
int argc; /* Num of arguments of current command. */
robj **argv; /* Arguments of current command. */
struct redisCommand *cmd, *lastcmd; /* Last command executed. */
...
/* Response buffer */
int bufpos;
char buf[PROTO_REPLY_CHUNK_BYTES];
} client;
除了上面提到的输入缓冲区 querybuy 属性、命令参数数组 argv 和命令个数 argc,还有 指向当前数据库的指针 *db、客户端名称 *name、命令指针 cmd、输出缓冲区 bufpos 和 buf[] 等。
三、命令执行器:查找命令实现
命令执行器首先要做的就是根据客户端状态的 argv[0] 参数,在命令表中找到对应的命令结构体,并保存在属性 cmd 中。其中命令表是一个字典结构,字典的 key 是每个命令本身,如 set、get、llen 等,值是每个对应的 struct redisCommand 结构,每个 struct redisCommand 结构体记录了一个 redis 命令的实现信息。结构体定义如下:
typedef void redisCommandProc(client *c);
struct redisCommand {
char *name;
redisCommandProc *proc;
int arity;
char *sflags; /* Flags as string representation, one char per flag. */
uint64_t flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
...
};
四、命令执行器:执行预备操作
目前为止,服务器已将执行命令所需要的参数、参数个数、命令实现函数都记录下来了,但是在真正执行之前,还需要做些预备操作。从而确保命令得以正确顺利地被执行,如:
- 检查客户端状态的 cmd 指针是否为 null;
- 根据客户端 cmd 属性指向的 redisCommand 结构体中的 arity 属性,检查命令请求参数个数是否正确;
- 检查客户端是否通过了身份验证;
- 检查服务器是否正在进行数据载入,如果正在载入,客户端发送的命令必须带有 1 标识才会被服务器执行,其他都会被拒绝;
- 检查服务器是否打开了监视器功能,如果有打开,则需要将命令参数等信息发送给监视器。
诸如以上种种,只有当以上预备操作处理以后,服务器才开始真正执行命令。
五、命令执行器:调用命令实现函数
在查找命令实现时,已经将需要执行的命令实现保存在属性 cmd 中了,这时只要执行如下语句即可:
client->cmd->proc(client);
命令参数信息可通过参数 client 获取,在命令实现函数对其处理并响应后,响应信息会保存到客户端状态的输出缓冲区中,也就是结构体 struct client 的 buf 和 reply 属性。之后实现函数还会为客户端的套接字关联命令回复处理器,当客户端的套接字变为可写状态时,这个处理器负责将响应返回给客户端。注意这个响应仍是协议格式。
六、命令执行器:执行后续操作
在执行完实现函数之后,服务器还需要执行一些后续工作:
- 如果服务器开启了慢查询日志功能,慢查询模块就会检查是否需要将刚刚执行完的命令添加到慢查询日志中;
- 根据刚刚执行命令耗费时长,更新被执行命令的 struct redisCommand 结构体的 milliseconds 属性,并对计数器 calls 属性加1;
- 如果服务器开启了 aof 持久化功能,那么 aof 持久化模块会将刚刚执行的命令请求写入 aof 缓冲区中;
- 如果有其它从服务器正在复制当前服务器,那么服务器会将刚刚执行的命令传播给所以从服务器。
当以上操作都执行完以后,服务器对于当前命令的执行就算结束了,之后服务器会继续从文件事件处理器中取出下一条命令请求并处理。
七、客户端接收并打印
当客户端收到协议格式的命令回复后,会将内容转成人类可读的格式,最后再打印出来。
redis> set key value
1
至此,以上就是 redis 客户端和服务器执行命令请求的整个过程,欢迎交流。