一条命令的执行过程有很多细节,但大体可以分为:客户端先将用户输入的命令转化为 Redis 相关的通讯协议,再用 socket 连接的方式将内容发送给服务器端。服务器端在接收到相关内容后,则是先将内容转化为具体的命令,再判断用户授权信息和其它相关信息,当验证通过时会执行最终命令,命令执行完毕后会进行相关的信息记录和数据统计,然后再把执行结果发送给客户端。这便是一条命令执行的生命周期,如果是集群模式的话,主节点还会将命令同步到子节点。
用一张图来看一下整体流程,然后再对每一个细节进行拆解。
Redis 的执行可以分为以下几个步骤:
步骤一:用户输入一条命令。
步骤二:客户端先将命令转化为 Redis 协议,然后再通过 socket 发送给服务器端。
客户端和服务器是基于 socket 进行通信的,服务器端在初始化的时候会创建一个用于监听的 socket,来监听客户端的 socket 连接,源码如下:
void initServer(void) {
//......
// 开启 Socket 事件监听
if (server.port != 0 &&
listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
exit(1);
//......
}
socket 小知识:每个 socket 被创建后,会分配两个缓冲区,分别是输入缓冲区和输出缓冲区。写入函数不会立即向网络中传输数据,而是先将数据写入到缓冲区中,再由操作系统内核按照 TCP 协议将数据从缓冲区发送到目标机器。而一旦数据写入到缓冲区,那么函数就可以成功返回,不管数据有没有到达目标机器,也不管数据何时到达,这些都是 TCP 协议负责的事情。另外,数据有可能刚被写入到缓冲区就发送到网络,也可能在缓冲区中不断积压、然后多次写入的数据一次性发送到网络,至于到底是哪种情况则取决于当前的网络情况、当然线程是否空闲等诸多因素,不由程序猿控制。读取函数也是如此,它也是从缓冲区中读取数据,而不是直接从网络中读取。
相当于面前有一个小箱子(缓冲区),写数据写到箱子里即可,读数据也从箱子里面读。至于写入函数往箱子里面写的数据由谁负责运出去,读取函数从箱子里面读取的数据又是由谁放进来的,这些我们都不需要关系,因为这是操作系统内核干的事情。
当 socket 成功连接(其实当我们输入 redis-cli、然后回车的时候就已经建立连接了)之后,将我们输入的命令转化为 Redis 通讯协议(RESP 协议,REdis Serialization Protocol),通过 socket 发送给服务器端,这个通讯协议是为了保证服务器能够以最快的速度理解命令的含义而制定的。如果没有这个通讯协议,那么 Redis 服务器端需要遍历所有的字符来确认此条命令的含义,这样会加大服务器端的运算;而直接发送通讯协议,相当于把服务器端的解析工作交给了每一个客户端,这样会大幅度提高 Redis 的运行速度。例如:当我们输入 set key val 这个命令时,客户端会把命令转化为 *3\r\n$3\r\nSET\r\n$4\r\nKEY\r\n$4\r\nVAL\r\n
发送给服务器端。关于 Redis 的通讯协议,可以参考官网:https://redis.io/topics/protocol
,不过个人觉得没有多大意义,这个协议我们知道具体是做什么的就好,至于命令是怎么转化成协议,完全不需要关心。*
步骤三:服务器端接收到命令。
服务器会先去缓冲中读取信息,然后判断数据的大小是否超过了系统设置的值(默认是 1GB),如果大于此值就会返回错误信息,并关闭客户端连接。默认大小如图所示:
当数据大小验证通过之后,服务器端会对输入缓冲区中的请求命令进行分析,提取请求中包含的命令参数,存在 client(服务器端为会每个连接创建一个 Client 对象)的属性中。
步骤四:执行前所需的准备。
1. 判断是否为退出命令,如果是则直接返回。
2. 非 null 判断,检查 client 是否为 null,如果为 null 返回错误信息。
3. 获取执行命令,根据 client 存储的属性信息去 redisCommand 结构中查询执行命令。
4. 用户权限检验,未通过验证的客户端只能执行 AUTH(授权) 命令;如果未通过身份验证的客户端执行了 AUTH 之外的命令,那么返回错误信息。
5. 如果是集群模式,会把命令重定向到目标节点,如果是 master 主节点则不需要重定向。
6. 检查服务器最大内存限制,如果服务器端开启了最大内存限制,会先检查内存大小,如果内存使用超过了最大范围那么会进行相应的回收操作。
7. 持久化检测,检查服务器是否开启了持久化和持久化出错停止写入配置,如果开启了此配置并且有持久化失败的情况,则禁止执行写命令。
8. 集群模式"最少从节点验证",如果是集群模式并且配置了 repl_min_slaves_to_write(最少从节点写入),当从节点数量小于配置项时,会禁止执行写命令。
9. 只读从节点验证,当此服务器为只读从节点时,只接受 master 的写命令。
10. 客户端订阅判断,当客户端正在订阅频道时,只会执行部分命令(SUBSCRIBE,PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE),其它命令会拒绝执行。
11. 从节点状态校验,当服务器为 slave 并且没有连接 master 时,只会执行状态查询命令,如:info 等。
12. 服务器初始化校验,当服务器正在启动时,只会执行 loading 标志的命令,其它命令会被拒绝。
13. lua 脚本阻塞校验,当服务器因执行 lua 脚本而阻塞时,只会执行部分命令。
14. 事务命令校验,如果执行的是事务命令,可开始事务时会将命令放入等待队列。
15. 监视器(monitor) 判断,如果服务器打开了监视器功能,那么服务器也会把执行命令和相关参数发送给监视器(监视器是用来监控服务器运行状态的)
当服务器经历完上面的操作后,就可以真正执行命令了。
步骤五:执行最终命令,调用 RedisCommand 中的 proc 函数执行命令。
步骤六:执行完后相关记录和统计。
1. 检查慢查询是否开启,如果开启,会记录慢查询日志;
2. 检查统计信息是否开启,如果开启的话会记录一些统计信息,例如执行命令所耗费的时长、以及计数器(calls)会自增 1;
3. 检查持久化功能是否开启,如果开启则会记录持久化信息;
4. 如果有其它"从服务器"正在复制"当前服务器",则会将刚刚执行的命令传播给其它从服务器;
步骤七:返回结果给客户端。
命令执行完之后,服务器会通过 socket 的方式把执行结果发送给客户端,客户端再将结果展示给用户,至此一条命令就结束了。
总结
当用户输入一条命令之后,客户端会将命令转化成 Redis 协议,然后以 socket 的方式发送值服务器端。服务器端在接收到数据之后,会先将协议转换成命令,然后进行各种验证来保证命令能够正确并且安全的执行,当验证处理完之后,会调用具体的方法执行此条命令,执行完毕之后再进行相关的统计和记录,最后把执行结果返回给客户端。整个流程,如图所示:
更多执行细节,可以在 Redis 源码文件 server.c 中查看。