参考:<<Redis设计与实现>>
- 注:这本书是基于Redis3.0版本写的,和后面的版本有点差异
Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
命令请求的执行过程
一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。如客户端执行了以下命令:
redis> set key value
OK
那么从客户端发送set key value
命令到获得回复OK期间,客户端和服务器共需要执行以下操作:
- 客户端向服务器发送命令请求
set key value
- 服务器接收并处理客户端发来命令请求,在数据库中设置操作,并产生命令回复OK
- 服务器将命令回复OK发送给客户端
- 客户端接收服务器返回的命令回复OK,并将这个回复打印出来
以上是大体的执行步骤,详细信息在下面的内容中进行说明。
1.1 发送命令请求
客户端发送命令请求,当用户在客户端输入一个命令请求时,客户端会把这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器,如下图所示:
示例:
在客户端执行set key value
命令,客户端会把命令转换成协议(*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
),然后将其发送给服务器。
1.2 读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而可读时,服务器将调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区(
querybuf
属性)里 - 对输入缓冲区里的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的
argv
属性和argc
属性里 - 调用命令执行器,执行客户端指定的命令
示例:
以上一节的SET命令为例,下图展示了程序将命令请求保存到客户端状态的输入缓冲区后客户端状态:
之后程序对输入缓冲区中的协议进行分析,将分析结果保存到客户端状态的argv属性和argc属性里,如下图所示:
之后,服务器将通过调用命令执行器来完成最终的执行命令。
1.3 命令执行器(1) - 查找命令的实现
命令执行器首先根据客户端状态的argv[0]参数,在命令表(command table)里查找参数相对应的命令,将找到的命令保存到客户端状态的cmd属性里。
命令表是一个字典:
- 字典的键是命令名字,如set、get、del等;
- 字典的值是一个redisCommand结构,该结构记录了Redis命令的实现信息。
redisCommand结构主要属性如下表所示:
属性名 | 类型 | 作用 |
---|---|---|
name | char* | 命令的名字,比如"set" |
proc | redisCommandProc* | 函数指针,指向命令的实现函数,比如setCommand |
arity | int | 命令参数的个数,用于检查命令请求的格式是否正确。如果值为-N,表示参数的数量大于等于N(命令的名字本身也是一个参数) |
sflags | char* | 字符串形式的标识值。这个值记录了命令的属性,如读命令、写命令、是否允许在载入时或Lua脚本中使用 |
flags | int | 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查使用该属性而不是sflags属性,因为对二进制标识的检查可以方便地通过& ^ ~ 等操作来完成 |
calls | long long | 服务器总共执行这个命令次数 |
milliseconds | long long | 服务器执行这个命令所耗费的总时长 |
redisCommandProc类型的定义为:
typedef void redisCommandProc(redisClient *c);
sflags属性可以使用的标识值及其意义如下表所示:
标识 | 意义 | 带有这个标识的命令 |
---|---|---|
w | 写入命令,可能会修改数据库 | SET RPUSH DEL等 |
r | 只读命令,不会修改数据库 | GET STRLEN EXISTS等 |
m | 命令可能会占用大量内存,执行前检查服务器内存使用情况,如果内存不足则进制执行 | SET APPEND RPUSH LPUSH SADD SINTERSTORE等 |
a | 管理命令 | SAVE BGSAVE SHUTDOWN等 |
P | 发布与订阅方面的命令 | PUBLISH SUBSCRIBE PUBSUB等 |
s | 命令不可以在Lua脚本中使用 | BRPOP BLPOP BRPOPLPUSH SPOP等 |
R | 随机命令,对于相同的数据集和相同的参数,命令返回的结果可能不同 | SPOP SRANDMEMBER SSCAN RANDOMKEY等 |
S | 在Lua脚本中使用这个命令时,对这个命令输出结果进行排序 | SINTER SUNION SDIFF SMEMBERS KEYS等 |
l | 命令可以在服务器载入数据的过程中使用 | INFO SHUTDOWN PUBLISH等 |
t | 允许从服务器在带有过期数据时使用的命令 | SLAVEEOF PING INFO等 |
M | 命令在监视器(monitor)模式下不会自动被传播 | EXEC |
以SET、GET命令示例redisCommand结构:
- SET
- 实现函数为setCommand
- 命令参数为-3:表示命令接受3个或以上的参数
- 命令标识为wm:表示SET命令是一个写入命令,并且在执行这个命令之前,服务器应该对内存占用状况进行检查,因为这个命令可能会占用大量内存
- GET
- 实现函数为getCommand
- 命令参数为2:表示该命令只接受2个参数
- 命令标识为r:标识是一个只读命令
GET、SET命令在命令表中结构如下图所示:
继续之前SET命令的例子,命令执行器首先根据客户端状态的argv[0]参数,在命令表里查找"set"键对应的redisCommand结构,然后客户端状态的cmd属性指向该结构。如下图所示:
1.4 命令执行器(2) - 执行预备操作
在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:
- 1.检查客户端状态的
cmd
指针是否指向NULL:如果是的话,说明用户输入的命令名字找不到相对应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误 - 2.根据客户端
cmd
属性指向的redisCommand结构的arity
属性,检查命令请求所给定的参数是否正确。当参数不正确时,不再执行后续步骤,直接向客户端返回一个错误。如arity
属性为-3,那么用户输入的命令参数个数大于3个才可以。 - 3.检查客户端是否已经通过了身份验证。未通过身份验证的客户端只能执行AUTH命令,如果未通过身份验证的客户端试图执行其他命令,服务器会向客户端返回一个错误。
- 4.如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器内存占用情况,并在有需要的时候进行内存回收,使得接下来的命令可以顺利执行。如果内存回收失败,就不再执行后续步骤,向客户端返回一个错误。
- 5.如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgsave-error功能,而且服务器即将执行的是一个写命令,那么服务器拒绝执行这个命令,并向客户端返回一个错误。
- 6.如果客户端正在用SUBSCRIBE命令订阅频道,或正在用PSUBSCRIBE命令订阅模式,那么服务器只会执行客户端发来的SUBSCRIB、PSUBSCRIBE、UNSUBSCRIB、PUNSUBSCRIBE四个命令,其他命令都会被服务器拒绝。
- 7.如果服务器正在进行数据载入,那么客户端发送的命令必须带有 l 标识(比如INFO、SHUTDOWN、PUBLISH等)才会被服务器执行,其他命令都会被服务器拒绝。
- 8.如果服务器因为执行Lua脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会被服务器拒绝。
- 9.如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令都会被放进事务队列中。
- 10.如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。
当完成了以上预备操作后,服务器就可以开始真正执行命令。
1.5 命令执行器(3) - 调用命令的实现函数
由于命令的实现已经保存到了客户端状态的cmd属性、命令参数以及参数个数保存到了客户端状态的argv和argc属性中,所以当服务器要执行命令时,只需要 客户端状态的指针 作为参数即可。
以之前的SET命令为例,客户端包含了命令实现、参数和参数个数的状态如下图所示:
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里(buf
和reply
属性),之后实现函数会为客户端的套接字关联命令回复处理器,该处理器负责将命令回复返回给客户端。
之前的示例中,函数调用setCommand(client)
会产生一个"+OK\r\n"
回复,这个回复会被保存到客户端状态的buf
属性里面。如下图所示:
1.6 命令执行器(4) - 执行后续工作
在执行完实现函数之后,服务器还需要执行一些后续操作:
- 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志
- 根据刚刚执行命令锁耗费的时间,更新被执行命令的redisCommand结构的
milliseconds
属性,并将该结构中的calls
计数+1。 - 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里。
- 如果有其他从服务器正在复制当前服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。
当上述操作都执行完,服务器对于当前命令的执行就结束了,之后服务器可以继续从文件事件处理器中取出并处理下一个命令请求。
1.7 将命令发送给客户端
- 命令实现函数会将命令回复保存到客户端的输出缓冲区里,并为客户端的套接字关联命令回复处理器。
- 当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
- 当命令回复发送完毕后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。
1.8 客户端接收并打印命令回复
当客户端收到协议格式的命令回复之后,它会将这些回复转换成可读的格式并打印出来。流程如下图所示:
之前的示例中,客户端会接收到服务器发来的"+OK\r\n"
协议回复时,它会将回复转换成OK\n
,然后打印:
redis> SET KEY VALUE
OK
以上就是Redis客户端和服务器执行命令请求的整个过程。