发送命令请求
当用户在客户端中键入一个命令请求时, 客户端会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器。
读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求, 并将其保存到客户端状态的输入缓冲区里面。
- 对输入缓冲区中的命令请求进行分析, 提取出命令请求中包含的命令参数, 以及命令参数的个数, 然后分别将参数和参数个数保存到客户端状态的
argv
属性和argc
属性里面。 - 调用命令执行器, 执行客户端指定的命令。
命令执行器:查找命令实现
命令执行器要做的第一件事就是根据客户端状态的 argv[0]
参数, 在命令表(command table)中查找参数所指定的命令, 并将找到的命令保存到客户端状态的 cmd
属性里面。
命令表是一个字典, 字典的键是一个个命令名字,比如 "set"
、 "get"
、 "del"
,等等; 而字典的值是一个个 redisCommand
结构, 每个 redisCommand
结构记录了一个 Redis 命令的实现信息。
命令名字的大小写不影响命令表的查找结果
因为命令表使用的是大小写无关的查找算法, 无论输入的命令名字是大写、小写或者混合大小写, 只要命令的名字是正确的, 就能找到相应的 redisCommand 结构。
比如说, 无论用户输入的命令名字是 "SET" 、 "set" 、 "SeT" 又或者 "sEt" , 命令表返回的都是同一个 redisCommand 结构。
redis> SET msg "hello world"
OKredis> set msg "hello world"
OKredis> SeT msg "hello world"
OKredis> sEt msg "hello world"
OK
命令执行器:执行预备操作
到目前为止, 服务器已经将执行命令所需的命令实现函数(保存在客户端状态的 cmd
属性)、参数(保存在客户端状态的 argv
属性)、参数个数(保存在客户端状态的 argc
属性)都收集齐了, 但是在真正执行命令之前, 程序还需要进行一些预备操作, 从而确保命令可以正确、顺利地被执行, 这些操作包括:
- 检查客户端状态的
cmd
指针是否指向NULL
, 如果是的话, 那么说明用户输入的命令名字找不到相应的命令实现, 服务器不再执行后续步骤, 并向客户端返回一个错误。 - 根据客户端
cmd
属性指向的redisCommand
结构的arity
属性, 检查命令请求所给定的参数个数是否正确, 当参数个数不正确时, 不再执行后续步骤, 直接向客户端返回一个错误。 比如说, 如果redisCommand
结构的arity
属性的值为-3
, 那么用户输入的命令参数个数必须大于等于3
个才行。 - 检查客户端是否已经通过了身份验证, 未通过身份验证的客户端只能执行 AUTH 命令, 如果未通过身份验证的客户端试图执行除 AUTH 命令之外的其他命令, 那么服务器将向客户端返回一个错误。
- 如果服务器打开了
maxmemory
功能, 那么在执行命令之前, 先检查服务器的内存占用情况, 并在有需要时进行内存回收, 从而使得接下来的命令可以顺利执行。 如果内存回收失败, 那么不再执行后续步骤, 向客户端返回一个错误。 - 如果服务器上一次执行 BGSAVE 命令时出错, 并且服务器打开了
stop-writes-on-bgsave-error
功能, 而且服务器即将要执行的命令是一个写命令, 那么服务器将拒绝执行这个命令, 并向客户端返回一个错误。 - 如果客户端当前正在用 SUBSCRIBE 命令订阅频道, 或者正在用 PSUBSCRIBE 命令订阅模式, 那么服务器只会执行客户端发来的 SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 四个命令, 其他别的命令都会被服务器拒绝。
- 如果服务器正在进行数据载入, 那么客户端发送的命令必须带有
l
标识(比如 INFO 、 SHUTDOWN 、 PUBLISH ,等等)才会被服务器执行, 其他别的命令都会被服务器拒绝。 - 如果服务器因为执行 Lua 脚本而超时并进入阻塞状态, 那么服务器只会执行客户端发来的 SHUTDOWN nosave 命令和 SCRIPT KILL 命令, 其他别的命令都会被服务器拒绝。
- 如果客户端正在执行事务, 那么服务器只会执行客户端发来的 EXEC 、 DISCARD 、 MULTI 、 WATCH 四个命令, 其他命令都会被放进事务队列中。
- 如果服务器打开了监视器功能, 那么服务器会将要执行的命令和参数等信息发送给监视器。
当完成了以上预备操作之后, 服务器就可以开始真正执行命令了。
命令执行器:调用命令的实现函数
在前面的操作中, 服务器已经将要执行命令的实现保存到了客户端状态的 cmd
属性里面, 并将命令的参数和参数个数分别保存到了客户端状态的 argv
属性和 argc
属性里面, 当服务器决定要执行命令时, 它只要执行以下语句就可以了:
// client 是指向客户端状态的指针client->cmd->proc(client);
因为执行命令所需的实际参数都已经保存到客户端状态的 argv
属性里面了, 所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。
命令执行器:执行后续工作
在执行完实现函数之后, 服务器还需要执行一些后续工作:
- 如果服务器开启了慢查询日志功能, 那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
- 根据刚刚执行命令所耗费的时长, 更新被执行命令的
redisCommand
结构的milliseconds
属性, 并将命令的redisCommand
结构的calls
计数器的值增一。 - 如果服务器开启了 AOF 持久化功能, 那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面。
- 如果有其他从服务器正在复制当前这个服务器, 那么服务器会将刚刚执行的命令传播给所有从服务器。
当以上操作都执行完了之后, 服务器对于当前命令的执行到此就告一段落了, 之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。
将命令回复发送给客户端
前面说过, 命令实现函数会将命令回复保存到客户端的输出缓冲区里面, 并为客户端的套接字关联命令回复处理器, 当客户端套接字变为可写状态时, 服务器就会执行命令回复处理器, 将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后, 回复处理器会清空客户端状态的输出缓冲区, 为处理下一个命令请求做好准备。
客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后, 它会将这些回复转换成人类可读的格式, 并打印给用户观看(假设使用的是 Redis 自带的 客户端)
以上就是 Redis 客户端和服务器执行命令请求的整个过程了。