客户端将协议格式发送给服务端,此时是发送在连接的套接字那里,然后套接字就会产生AE_READABLE事件(表示可读状态),然后就会调用命令处理器来执行以下操作

  • 读取套接字中协议格式的命令请求,并将其保存到客户端状态(RedisClient)的输入缓冲区里面,即querybuf属性,是一个SDS类型的变量(回看客户端的文章)
  • 对输入缓冲区中的命令请求进行分析(因为是协议格式,所以要进行分析),提出命令请求中的所有命令参数,以及统计命令参数个数,然后将参数分别保存到客户端状态里面的argv属性和argc属性
  • 服务器调用命令执行器,服务器执行客户端指定的命令。
命令执行器的步骤

命令执行器再拿到argv数组时,要进行下列的步骤

  • 查找命令的实现
  • 执行预备操作
  • 调用命令的实现函数
  • 执行后续工作
查找命令的实现

argv数组里面,第一个元素保存的就是命令的类型(set、get、zadd等命令),在RedisServer(服务器状态)里面保存有一个名为cmd属性,cmd属性是字典类型的,key就是这些命令名字构成的字符串对象,Value则是对应的RedisCommand结构,该结构是记录了命令的具体实现的。

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文件中,进行持久化
  • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有的从服务器
将命令回复发送给客户端