Redis(十八):服务器
命令执行器的步骤
命令执行器再拿到 argv 数组时,要进行下列的步骤
查找命令的实现
执行预备操作
调用命令的实现函数
执行后续工作
查找命令的实现
argv 数组里面,第一个元素保存的就是命令的类型(set、get、zadd 等命令),在 RedisServer(服务器状态)里面保存有一个名为 cmd 属性,cmd 属性是字典类型的,key 就是这些命令名字构成的字符串对象,Value 则是对应的 RedisCommand 结构,该结构是记录了命令的具体实现的。
| 属性名 | 类型 | 作用 |
| --- | --- | --- |
| name | char * | 命令的名字,比如 set |
| arity | int | 命令参数的个数,用来检查命令请求的格式是否正确。对于可以给多个参数的命令,该属性可能为-N,表示参数的数量大于等于 N。要注意的一点是,命令参数的个数要包含命令的名字,比如 set key value,set 命令的参数个数就是 3 个 |
| proc | redisCommandProc* | 是一个函数指针,指向命令的实现函数 |
| sflags | char * | 是一个标识值,标识该命令是读还是写命令,并且可以标识该命令可不可以在 lua 脚本使用,是一个字符串 |
| flags | int | 对 sflags 标识进行分析得出的二进制标识,由程序自动生成的,服务器校验时都是根据 flags 属性的,而不是根据 sflags 属性,因为 flags 属性是一个整形,可以通过一些二进制运算来进行分析判断 |
| calls | long long | 服务器总共执行了多少次这个命令 |
| milliseconds | long long | 服务器执行这个命令所耗费的总时长 |
slfags 属性的标识
| 标识 | 意义 | 带有该标识的命令 |
| --- | --- | --- |
| w | 写入命令 | SET RPUSH DEL 等 |
| r | 只读命令 | GET STRLEN EXISTS 等 |
| m | 可能会占用大量内存,运行时要检查内存是否足够 | LPUSH SADD 等一些 O(N)复杂度的命令 |
| a | 管理命令 | SAVE ;BGSAVE;SHUTDOWN 等 |
| p | 发布订阅功能方面的命令 | PUBLIC SUBSCRIBE 等 |
| s | 该命令不可以在 Lua 脚本中使用 | |
| R | 随机命令,即相同的命令,返回值可能不同 | SPOP,RANDOMKEY 等 |
| S | 当在 Lua 脚本中使用该命令时,对这个命令的输出结果进行一次排序,让命令的结果有序 | |
| I | 命令可以在服务器载入数据时使用 | |
| t | 该命令允许从服务器在带有过期数据时使用 | |
| M | 这个命令在监视器模式下不会自动被传播 | |
命令表使用的是大小写无关的查找算法,无论输入的命令还是大写、小写或者混合大小写,只要名字是正确的,就可以找到相应的 RedisCommand
执行预备操作
此时,服务器已经将执行命令所需的命令实现函数(cmd 属性)、命令参数与命令参数个数(argv,argc)都已经收集齐了,但是此时并不是立即开始执行操作,程序还会进行一些预备操作,从而确保命令可以正确、顺利地
被执行,这些命令包括以下
检查客户端是否已经通过验证,即看客户端的里面有一个名为 authenticated 的属性,如果为 1,代表已经验证,不为 1,就代表未验证,返回一个错误
检查客户端的 cmd 指针是否为 NULL,即是否找到了对应的实现函数,如果为 NULL,代表命令输入有误,那么服务器并不会去执行,并且向客户端返回一个错误
经过上个判断,cmd 指针不为 NULL,也就是命令是正确的,此时要判断参数是否合理,通过 RedisCommand 结构的 arity 属性和 argc 属性进行比较,如果参数个数不正确,也是不会去执行,并且向客户端返回错误,注意 arity 属性是可以为负数的,如果为-N,就看 argc 是否大于等于 N
最后一步是检查是否打开了 maxmemory 功能,如果打开了,服务器会先查看当前的内存使用情况,并且会在有需要时进行内存回收,让实现命令的时候可以顺利执行,如果内存不够且回收失败,就不再执行后续步骤,向客户端返回错误
在配置文件有一个选项为 stop-writes-on-bgsave-error,如果该选项为 yes,那么就代表着如果 RDB 异步持久化失败,就不会执行写操作,所以,当上一条命令为 BGSAVE 时且执行失败后,如果下一条还是写命令就不会执行,服务端拒绝执行操作
如果客户端当前正在用 SUBSCRIBE 命令订阅频道,或者使用 PSUBSCRIBE 命令订阅模式,那么服务器对于这个客户端只会执行 SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE,这 4 个命令(第一个是订阅频道,第二个是订阅指定模式的频道,第三个是退订指定频道,第四个是退订指定模式的频道),其他命令都会被拒绝
如果服务器此时正在进行数据载入,那么 slags 属性必须为 I,否则不会执行
如果客户端因为执行 Lua 脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的 SHUTDOWN nosave 和 SCRIPT KILL 命令,其他命令都会被服务器拒绝
如果客户端正在执行事务,那么服务器只会执行客户端发来的 EXEC、DISCARD、MULTI、WATCH 命令这 4 个命令(这 4 个命令都是事务相关命令,MULTI 开启事务,DISCARD 取消事务,EXEC 事务结束,WATCH 在事务中监视指定 Key,如果 key 被修改,事务被打断,会回滚),其他命令都会被放进事务队列中
如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器
调用命令的实现函数
进行了上面一大串的预备操作,下面可以正式开始执行操作
客户端要执行的命令实现已经保存到 cmd 属性里面,并将命令的参数和参数个数分别保存到了客户端状态的 argv 属性和 argc 属性中,当服务器决定要执行命令,只需要执行 cmd 里面的 RedisCommand 里面的 proc 属性
client->cmd->proc(client) //将整个客户端状态作为参数传给 proc
被调用的命令实现函数会执行指定的操作,并且会产生相应的命令回复,将这些回复保存到 RedisClinet 的 buf 和 reply 属性中去,之后还会为客户端的套接字关联命令回复处理器(关联 AE_WRITEABLE 事件),之后客户端请求读取回复时是要用这个处理器将命令回复返回给客户端。
执行后续工作
执行完实现函数之后,服务器还需要进行一些后续工作
如果服务器开启了慢查询日志功能,那么服务器的慢查询日志模块会检查是否需要为刚刚执行完的命令请求去添加一条慢查询日记记录
计算出执行命令所耗费的时长,更新 RedisCommand 结构里面的 millseconds 属性,并且让 calls 属性进行自增 1(该 RedisCommand 不是该客户端独有的,而是全局的)
如果服务器开启了 AOF 持久化功能,那么 AOF 持久化模块会将执行的命令请求写入到 AOF 文件中,进行持久化
如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有的从服务器
将命令回复发送给客户端
执行完后续工作后,就可以进行回复了,但回复也是要客户端先进行请求,服务器才会进行回复
命令实现函数将回复信息保存在客户端状态里面的 buf 或者 reply 属性中,并且为客户端的套接字绑定了命令回复处理器,关联的是 AE_WRITEABLE 事件,此时用户连接套接字并且请求回复,发产生 AE_WRITEABLE 属性,该事件就会被窃听到,然后就会调用命令回复处理器,将客户端状态里面的回复发送给客户端。
回复完后,命令回复处理器还会将该客户端状态里面的 buf 和 reply 清空,并且解除套接字与回复命令套接字的关联,为下一次的命令请求做好准备。
这里发送的回复,也是按照协议格式进行发送的
客户端接收并打印命令回复
当客户端那边接收到回复后,会将协议格式的回复进行转换,转换成可读形式,并且打印给用户去观看。
评论