当前位置: 首页 > 后端技术 > PHP

跟随大斌阅读源码-Redis3-服务端如何响应客户端请求?(下)

时间:2023-03-29 19:51:46 PHP

继续我们上一节的讨论。服务器启动,客户端发送命令。接下来,就是服务器“执行”的时候了。1服务器处理服务器读取命令请求后,会进行一系列的处理。1.1读取命令请求当客户端和服务器之间的socket由于客户端写入变为可读时,服务器会调用commandrequesthandler执行以下操作:读取socket中的命令请求,并保存到客户端状态的输入缓冲区。分析输入缓冲区中的命令请求,提取命令请求中包含的命令参数和参数个数,然后将参数和参数个数分别保存在客户端态的argv属性和argc属性中。调用命令执行器执行客户端指定的命令。将上述SET命令保存到客户端状态的输入缓冲区后,客户端状态如图4所示。之后,分析程序会对输入缓冲区中的协议进行分析,并将结果保存在的argv和argc属性中客户端,如图5所示:之后,服务器将通过调用命令执行器执行剩余步骤来执行命令。1.2搜索命令的实现命令执行器首先需要根据argv[0]参数在命令表(commandtable)中搜索参数指定的命令,并将找到的命令保存到cmd属性中。命令表是一个字典,字典的键是命令名,如“SET”、“GET”等。字典的值是一个redisCommand结构体,每个redisCommand结构体记录了Redis命令的执行信息。源码如下:#server.h/redisCommandstructredisCommand{char*name;//命令名称。如“SET”redisCommandProc*proc;//对应函数指针,指向命令的实现函数。比如SETintarity对应的setCommand函数;//命令的网格参数个数。用于检查命令请求的格式是否合法。//需要注意的命令名称也是一个参数。就像我们上面的SETKEYVALUE命令一样,其实就是三个参数。字符*标志;//字符串形式的标志值。命令的属性已记录。内部标志;//通过分析sflagsflags得到的binaryflags,由程序自动生成。查看命令时,实际使用的是这个字段redisGetKeysProc*getkeys_proc;//指针函数,通过这个方法来指定key的位置。int首键;//第一个键的位置intlastkey;//最后一个键的位置intkeystep;//按键之间的距离longlongmicroseconds,calls;//总调用时间和调用次数};,对于sflags属性,可用的flag值及其含义如下:FlagmeaningCommandwiththisflagw这是一个写命令,可能会修改数据库的SET,RPUSH,DEL等r这是一个读-onlycommand,not会修改数据库的GET,STRLEN等。m这个命令可能会占用大量的内存。执行者需要先检查内存使用情况。如果内存不足,则禁止执行此命令SET、APPEND、RPUSH、SADD等a这是管理命令SAVE、BGSAVE等p这是发布订阅功能命令PUBLISH、SUBSRIBE等.slua脚本BPOP、BLPOP等不能使用该命令R这是随机命令。对于相同的数据集,相同的参数,返回的结果可能不同SPOP,SRANDMEMBER等。S在lua脚本中使用该命令时,对返回结果进行排序,使结果按SINTER,SUNION等排序l这个可以在服务器加载command在输入数据的过程中,使用INFO、PUBLISH等t命令,允许在从库中有过期数据时使用SLAVEOF、PING等命令。在监控模式下,M命令不会自动传播。在EXECk集群模式下,如果对应的slot标志位为“Import”,则接受restore-askingF这条命令。该命令应在程序执行时立即执行SETNX、GET等命令。]作为输入,在command表中查找时,command表返回“set”键对应的redisCommand结构,client态的cmd指针会指向这个redisCommand结构。如图7所示:需要注意的是,对于Redis,命令名大小写不影响命令表的查找结果,即命令名不区分大小写。执行SET和set,Set会得到相同的结果。1.3执行准备操作至此,服务器已经初始化了执行命令所需的命令实现函数(clientcmd属性)、参数(clientargv属性)、参数个数(clientargc属性)。但在真正执行命令之前,程序还会进行一些准备操作,以保证命令能够正确、顺利地执行。前期操作包括:检查客户端的cmd指针是否指向NULL,如果是,说明用户输入的命令名没有对应的功能,服务器不再进行后续操作,并返回错误给客户端.根据客户端cmd属性指向的redisCommand结果的arity属性,检查命令请求中给出的参数个数是否正确。检查客户端是否已通过身份验证。未经身份验证的客户端只能执行AUTH命令。否则,将向客户端返回一个错误。如果服务器开启了maxmemory功能,在执行命令前,会先查看服务器的内存使用情况,必要时回收内存,以便下一条命令顺利执行。如果内存回收失败,则不会执行后续步骤,并会向客户端返回错误。如果服务器上次执行BGSAVE命令时发生错误,并且服务器开启了stop-writes-on-bgsave-error功能,并且要执行的命令是写命令,那么服务器会拒绝执行shoe命令并返回一个错误。如果客户端正在使用SUBSCRIBE和PSUBSCRIBE命令订阅频道或模式,服务器只会执行客户端发送的SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE和PUNSUBSCRIBE这四个命令,其他命令将被拒绝。如果服务端正在加载数据,则客户端发送的命令必须带有l标记才能被服务端执行。如果客户端正在执行一个事务,那么服务器只会执行EXEC、DISCARD、MULTI、WATCH这四个命令,其他的命令都会被放入事务队列中。如果服务器开启了监听功能,服务器会将要执行的命令、参数等信息发送给监听器。上述准备工作完成后,服务器开始真正执行命令。需要注意的是,上面列出的准备操作只是服务器在单机模式下的检查操作。如果是复制或者集群模式,准备操作会比较多。1.4调用命令的实现函数在前面的操作中,服务端已经在客户端结构体中保存了要执行的命令的实现、参数、参数个数。对于我们上面的SETKEYVALUE命令,图8包含了命令的实现、参数和参数个数结构:当服务端决定执行命令时,只需要执行如下语句://client是一个指向客户端状态的指针。server.c/call()客户端->cmd->proc(客户端);上面的执行语句实际上调用了setCommand函数(t_string.c)。被调用的命令实现函数会执行指定的操作并产生相应的命令回复,保存在客户端状态的输出缓冲区(bug属性和回复属性),然后实现函数会提供给客户端socket的话——关联的命令应答处理器由命令应答处理器返回给客户端。回到我们的示例,setCommand(client)将生成一个“+OKrn”回复,它存储在客户端的buf属性中。如图9所示:1.5执行后续工作函数执行后,服务端会进行一些后续工作,主要包括:如果服务端开启了慢日志功能,慢查询日志模块会检查是否刚刚执行的命令添加到慢查询日志。更新毫秒和调用redisCommand结构的属性。如果服务器开启了AOF持久化功能,AOF持久化模块会将刚刚执行的命令请求写入AOF缓冲区。如果其他服务器正在复制当前服务器,则该服务器会将刚刚执行的命令传播到所有从服务器。执行完上述后续操作后,执行执行命令。服务器可以继续处理后续命令。1.6向客户端发送命令回复在上述过程中,命令实现函数会将命令回复保存到客户端的输出缓冲区,并将命令回复处理器与客户端的套接字相关联。当客户端套接字变为可写时,服务器执行命令回复处理程序并将命令回复发送给客户端。当发送命令回复时,回复处理程序将更新客户端的输出缓冲区,以准备处理下一个命令请求。以图9所示的client状态为例,当client的socket变为可写状态时,commandreplyprocessor会向client发送协议格式的commandreply“+OKrn”。1.7源码解释命令处理请求,函数调用栈信息如图3-7-1:命令回复,函数调用栈信息如图3-7-2:2客户端接收并打印回复客户端后收到命令回复,它会将回复转换成我们可以阅读的格式并打印在屏幕上(对于redis-cli客户端),如图10所示。至此,我们已经完成了从发起一个命令开始的所有过程命令请求到收到回复。您对我们最初的问题有答案吗,服务器如何响应客户端请求?总结服务端通过networking.c/readQueryFromClient()读取并执行相应的命令。服务器通过networking.c/writeToClient()将命令回复发送给客户端。