命令请求的执行过程
使用SET KEY VALUE命令为例, 客户端和服务器共需要执行以下操作:
-
客户端向服务器发送命令请求
SET KEY VALUE -
服务器接收并处理客户端发来的命令请求
SET KEY VALUE, 在数据库中进行设置操作, 并产生命令回复OK -
服务器将命令回复OK发送给客户端
-
客户端接收服务器返回的命令回复
OK, 并将这个回复打印给用户观看
发送命令请求
Redis服务器的命令去请求来自Redis客户端, 当用户在客户端中键入一个命令请求时, 客户端将会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器。
SET KEY VALUE
这个命令转换成协议:
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
读取命令请求
当客户端与服务器之间的链接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:
-
读取套接字中协议格式的命令请求, 并将其保存到客户端状态的输入缓冲区里面
-
对输入缓冲区中的命令请求进行分析, 提取出命令请求包含的命令参数, 以及命令参数的个数, 然后分别将参数和参数保存到客户端状态的
argv属性和argc属性里面 -
调用命令处理器, 执行客户端指定的命令
命令执行器: 查找命令实现
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数, 在命令表中找参数所指定的命令, 并将找到的命令保存到客户端状态的cmd属性里面.
| 属性名 | 类型 | 作用 |
|---|---|---|
| name | char * | 命令的名字, 比如”set” |
| proc | redisCommandProc * | 函数指针, 指向命令的实现函数, 比如setCommand,redisCommandProc类型的定义为typedef void redisCommandProc(redisClient *c) |
| arity | int | 命令参数的个数, 用于检查命令请求的格式是否正确. 如果这个值为-N, 那么表示参数的数量大于等于N. |
| sflags | char * | 字符串形式的标示值, 这个值记录了命令的属性, 比如这个命令时写命令还是读命令, 这个命令是否允许在载入数据时使用, 这个命令是否允许在Lua脚本中使用等等 |
| flags | int | 对sflags标识进行分析得出的二进制标识, 由程序自动生成。服务器对命令标识进行检查时使用的是flags属性而不是sflags属性, 因为对二进制标识的检查可以方便地通过&,^,~等操作完成 |
| calls | long long | 服务器总共执行了多少次这个命令 |
| milliseconds | long long | 服务器执行这个命令所消耗的总时长 |
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, PUTLISH等等 |
| t | 这是一个允许从服务器在带有过期数据时使用的命令 | SLAVEOF, PING, INFO等等 |
| M | 这个命令在监视器模式下不会自动被传播 | EXEC |
命令执行器: 执行预备操作
到目前为止, 服务器已经将被执行命令实现函数, 参数, 参数个数都收集齐了, 但是在真正执行命令之前, 程序需要进行一些预备操作, 从而确保命令可以正确, 顺利地被执行, 这些操作包括:
-
检查客户端状态的
cmd指针是否指向NULL, 如果是的话, 那么说明用户输入的命令名字找不到响应的命令实现, 并向客户端返回一个错误 -
根据客户端CMD属性指向的
redisCommand结构的arity属性, 检查命令请求所给定的参数个数是否正确, 当参数个数不正确时, 不在执行后续操作步骤, 执行向客户端返回一个错误. -
检查客户端是否已经通过了身份验证, 位通过身份验证的客户端只能执行
AUTH命令, 如果未通过身份验证的客户端试图执行除AUTH命令之外的其他命令, 那么服务器将想客户端返回一个错误. -
如果服务器打开了
maxmemory功能, 那么在执行命令之前, 先检查服务器的内存占用情况, 并在需要时进行内存回收, 从而使得接下来的命令可以顺利执行。如果内存回收失败, 那么不再执行后续步骤, 像客户端返回一个错误 -
如果服务器上一次执行BGSAVE命令出错, 并且服务器打开了一个
stop-writes-on-bgsave-error功能, 而且服务器即将要执行的命令是一个写命令, 那么服务器将拒绝执行这个命令, 并向客户端返回一个错误 -
如果客户端当前正在使用
SUBSCRIBE命令订阅频道, 或者正在用PSUBSCRIBE命令订阅模式, 那么服务器只会执行客户端发来的SUBSCRIBE,PSUBSCRIBE,UNSUBSCRIBE,PUNSUBSCRIBE四个命令, 其他命令都会被服务器拒绝 -
如果服务器正在进行数据载入, 那么客户端发送的命令必须带有
l标识(比如INFO, SHUDOWN, PUBLISH)才会被服务器执行, 其他命令会被服务器拒绝 -
如果服务器因为执行
Lua脚本而超时并进入阻塞状态, 那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIBE KILL命令, 其他命令都会被服务器拒绝 -
如果客户端正在执行事务, 那么服务器只会执行客户端发来的
EXEC,DISCARD,MULTI,WATCH四个命令, 其他命令都会被放进事务队列中 -
如果服务器打开了监视器功能, 那么服务器会将要执行的命令和参数等信息发送给监视器, 当完成以上预备操作之后, 服务器就可以真正执行命令了.
NOTE: 以上只列出了服务器在单机模式下执行命令时的检查操作, 当服务器在复制模式或者集群模式下执行命令时, 预备操作还会更多一些.
命令执行器:调用命令的实现函数
被调用的命令会被放在client的cmd属性,以及对应的命令参水会放在argv和argc两个属相之中.
被调用的命令实现函数会执行指定的操作, 并产生响应的命令回复, 这些命令会被保存在客户端状态的输出缓冲区里面(buf属性和reply属性), 之后实现函数还会为客户端的套接字关联命令回复处理器, 这个处理器负责将命令回复返给客户端.
命令执行器: 执行后续工作
在执行完实现函数之后, 服务器还需要执行一些后续工作:
-
如果服务器开启了慢查询日志功能, 那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求天假一条新的慢查询日志
-
根据刚刚执行命令嗦消耗的时长, 更新被执行命令的
redisCommand结构的milliseconds属性, 并将命令的redisCommand结构的calls计数器增一 -
如果服务器开启了AOF持久化功能, 那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面
-
如果有其他从服务器正在复制当前的这个服务器, 那么服务器会将刚刚执行的命令传播给所有从服务器。
当以上操作都执行完了之后, 服务器对于当前命令的执行到此就告一段落, 之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。
将命令回复发送给客户端
命令实现函数会将命令回复保存到客户端的输出缓冲区里面, 并为客户端的套接字关联命令回复处理器, 当客户端套接字变为可写状态时, 服务器就会执行命令回复处理器, 将保存在客户端输出缓冲区中的命令回复发送给客户端.
客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后, 它会将这些回复转换成人类可读的格式.
serverCron函数
Redis服务器中的serverCron函数默认每隔100毫秒执行一次, 这个函数负责管理服务器的资源, 并保持服务器自身的良好运转。
更新服务器时间缓存
Reids服务器中不少功能需要获取系统的当前时间, 而每次获取系统的当前时间都需要执行一次系统调用, 为了减少系统调用的执行次数, 服务器状态中的unixname属性和mstime属性被用作当前时间的缓存:
struct redisServer {
// 保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;
// 保存了毫秒级精度的系统当前UNIX时间戳
}
因为serverCron函数默认会以每100毫秒执行一次,用于更新unixtime和mstime两个属性。
-
服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精度要求不高的功能上
-
对于为键设置过期时间、添加慢查询日志这种需要高精度时间的功能来说,
服务器还是会再次执行系统调用, 从而获得最准确的系统当前时间
更新LRU时钟
服务器状态中的lrulock属性保存了服务器的LRU时钟, 这个属性和上面介绍的unixtime属性,mstime属性一样, 都是服务器时间缓存的一种:
struct redisServer {
// 默认每10秒更新一次的时钟缓存, 用于计算键的空转(idle)时长
unsigned lruclock:22;
}
NOTE: 每个Redis对象都会有一个lru属性, 这个lru属性保存了对象最后一次被命令访问的时间.
当前的服务器的lruclock的值,可以通过INFO server查看具体的lru_clock属性查看.
更新服务器每秒执行命令次数
serverCron函数中的trackOperationsPerSecond函数会以每100毫秒一次的频率执行,这个函数的功能时以抽样计算的方式, 估算并记录服务器在最近一秒钟处理的命令请求数量, 这个值可以通过INFO status命令的instantaneous_ops_per_sec域查看:
trackOperationsPerSecond函数和服务器状态中四个ops_sec_开头的属性有关:
struct redisServer {
// 上一次进行抽样的时间
long long ops_sec_last_sample_time;
// 上一次抽样时, 服务器一致性命令的数量
long long ops_sec_last_sample_ops;
// REDIS_OPS_SEC_SAMPLES大小(默认值为16)的环形数组,数组中每个项纪录了一次抽象结果.
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES]
// ops_sec_samples数组的索引值,
// 每次抽样之后将值自增一
// 在值等于16时重置为0
// 让ops_sec_samples数组构成一个环形数组
int ops_sec_idx;
}
更新服务器内存峰值纪录
服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小:
struct redisServer {
// 已使用内存峰值
size_t stat_peak_memory;
}
每次serverCron函数执行时, 程序都会查看服务器当前使用的内存数量, 并与stat_peak_memory保存的值进行比较, 如果当前使用的内存数量比stat_peak_memory属性记录的值要大, 那么程序就将当前使用的内存数量记录到stat_peak_memory属性中
INFO memory命令的used_memory_peak和used_memory_peak_human两个域分别以两种格式记录了服务器的内存峰值.
处理SIGTERM信号
在启动服务器器时, Redis会为服务器进程的SIGTERM信号管理处理器sigtermHandler函数, 这个信号处理器负责在服务器接收到SIGTERM信号时, 打开服务器状态的shutdown_asap标识:
每次serverCron函数运行时, 程序都会对服务器状态的shutdown_asap属性进行检查, 并根据属性的值决定是否关闭服务器:
struct redisServer {
// 关闭服务器的标识
// 1 - 关闭服务器
// 0 - 不做动作
int shutdow_asap;
}
NOTE: 服务器在关闭自身之前会进行RDB持久化操作, 这也是服务器拦截
SIGTERM信号的原因。
管理客户端资源
serverCron函数每次执行都会调用clientCron函数, clientCron函数会对一定数量的客户端进行以下两个检查:
-
如果客户端与服务器之间的链接已经超时(很长一段时间里客户端和服务器都没有互动), 那么程序释放这个客户端
-
如果客户端在上一次执行请求命令之后, 输入缓冲区的大小超过了一定的长度, 那么程序会释放客户端当前的输入缓冲区, 并重新创建一个默认大小的输入缓冲区,从而防止客户端的输入缓冲区消耗了过多的内存.
管理数据库资源
serverCron函数每次执行都会调用databasesCron函数, 这个函数对服务器中的一部分数据库进行检查, 删除其中的过期键, 并在有需要时, 对字典进行收缩操作。
执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE命令的期间, 如果客户端向服务器发来BGREWRITEAOF命令, 那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行之后.
在服务器对象中的aof_rewrite_sheduled标识记录了服务器是否延迟了BGREWRITEAOF命令
struct redisServer {
// 如果值为1, 那么标识有``BGREWRITEAOF``命令被延迟了
int aof_rewrite_sheduled;
}
每次serverCron函数执行时, 函数都会检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行, 如果两个命令都没有在执行, 并且aof_rewrite_sheduled属性的值为1, 那么服务器就会执行之前被推延的BGREWRITEAOF命令。
检查持久化操作的运行状态
服务器状态使用rdb_child_pid属性和aof_child_pid属性标记执行BGSAVE命令和BGREWRITEAOF命令的子进程ID, 这两个属性也可以用于检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行:
struct redisServer {
// 记录执行BGSAVE命令的子进程的ID:
// 如果服务器没有在执行BGSAVE, 那么这个属性的值为-1
pid_t rdb_child_pid;
// 记录执行BGREWRITEAOF命令的子进程ID:
// 如果服务器没有在执行BGREWRITEAOF
// 那么这个属性的值为-1
pid_t aof_child_pid;
}
每次serverCron函数执行时, 程序都会检查rdb_child_pid和aof_child_pid两个属性的值, 只要其中一个属性的值不为-1, 程序就会执行wait3函数, 检查子进程是否有信号发来服务器进程:
-
如果有信号到达, 那么表示新的RDB文件已经生成完毕或者AOF文件已经重写完毕, 服务器需要进行相应的后续操作, 比如用心的RDB文件替换现有的RDB文件, 或者用重写后的AOF文件替换现有的AOF文件
-
如果没有信号到达, 那么标识持久化操作未完成, 程序不做动作。
另一方面, 如果rdb_child_pid和aof_child_pid两个属性的值都是-1, 那么表示服务器没有在进行持久化操作, 在这种情况下, 程序执行以下三个检查:
-
查看是否有
BGREWRITEAOF被延迟了,如果有的话, 那么开始一次新的BGREWRITEAOF操作 -
检查服务器的自动保存操作是否已经被满足, 如果条件满足, 并且服务器没有在执行其他持久化操作, 那么服务器开始一次新的
BGSAVE操作。(因为条件1可能会引发一次BGREWRITEAOF操作, 所以在这个检查中, 程序会再次确认服务器是否满足已经在执行持久化操作了) -
检查服务器设置的
AOF重写条件是否满足, 如果满足条件, 并且服务器没有在执行其他持久化操作, 那么服务器将开始一次性的BGREWRITEAOF操作(因为条件1和条件2都可能会引起新的持久化操作, 所以在这个检查中, 我们要再次确认服务器是否已经在执行持久化操作了)
将AOF缓冲区中的内容写入AOF文件
如果服务器开启了AOF持久化功能, 并且AOF缓冲区里面还有待写入的数据, 那么serverCron函数会调用响应的程序。 将AOF缓冲区的内容写入到AOF文件里面.
关闭异步客户端
在这一步, 服务器关闭那些输出缓冲区大小超过限制的客户端
增加cronloops计数器的值
服务器状态的cronloops属性记录了serverCron函数的执行的次数:
struct redisServer {
// serverCron 函数的运行次数计数器
// serverCron 函数每执行一次, 这个属性就增一
int cronloops;
}
cronloops属性目前在服务器中的唯一座椅, 就是在复制模块中实现每执行serverCron函数N次就执行一次指定代码.
初始化服务器
一个Redis服务器从启动到能够接受客户端的命令请求, 需要经过一系列的初始化和设置过程,
初始化服务器状态结构
初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态, 并为结构中的各个属性设置默认值.
void initServerConfig(void) {
// 设置服务器的运行id
getRandomHexChars(server.runid, REDIS_RUN_ID_SIZE);
// 为运行id加上结尾字符
server.runid(REDIS_RUN_ID_SIZE) = '\0';
// 设置默认配置文件路径
server.configfile = null;
// 设置默认服务器的频率
server.hz = REDIS_DEFAULT_HZ;
// 设置服务器的运行架构
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
// 设置默认服务器端口号
server.port = REDIS_SERVERPORT;
}
以下是initServerConfig函数完成的主要工作
-
设置服务器的运行id
-
设置服务器的默认运行频率
-
设置服务器的默认配置文件路径
-
设置服务器的运行架构
-
设置服务器的默认端口号
-
设置服务器的默认RDB持久化条件和AOF持久化条件
-
创建命令表
载入配置选项
载入服务器时, 用户可以通过给定配置参数或者指定配置来修改服务器的默认配置.
可以通过命令行指定的方式, 来实现配置参数的指定
redis-server --port 10086
初始化服务器数据结构
在之前执行initServerConfig函数初始化server状态时, 程序只创建了命令表一个数据结构, 不过除了命令表之外, 服务器状态还包含其他数据结构
-
server.clients 链表, 这个链表记录了所有与服务器相连的客户端的状态结构, 链表的每个节点都包含了一个redisClient结构实例.
-
server.db数组, 数组中包含了服务器的所有数据库 -
用于保存频道订阅信息的
server.pubsub_channels字典, 以及用于保存模式订阅信息的server.pubsub_patterns链表 -
用于执行
lua脚本的lua环境server.lua -
用于保存慢查询日志的
server.slowlog属性。
NOTE: 服务器在此时初始化数据结构原因在于, 服务器必须先载入用户指定的配置选项。 然后才能正确地对数据结构进行初始化。
除了初始化数据结构之外, initServer还进行了一些非常重要的设置操作, 其中包括:
-
为服务器设置进程信号处理器
-
创建共享对象: 这些对象包含Redis服务器经常用到的一些值, 比如包含
OK回复的字符串对象,包含ERR回复的字符串对象。包含整数1-10000的字符串对象等等。服务器通过重用这些共享对象来避免反复创建想听的对象 -
打开服务器的监听端口, 并为监听套接字关联链接应答事件处理器, 等待服务器正式运行时接收客户端的链接
-
为serverCron函数创建时间事件, 等待服务器正式运行时执行
serverCron函数 -
如果AOF持久化功能已经打开, 那么打开现有的AOF文件, 如果AOF文件不存在, 那么创建并打开一个新的AOF文件,为AOF写入做好准备
-
初始化服务器的后台I/0模块, 为将来的I/0操作做好准备。
还原数据库状态
在完成了对服务器状态server变量的初始化之后, 服务器需要载入RDB文件, 或者AOF文件, 并根据文件记录的内容来还原服务器的数据库状态.
根据服务器是否启用了AOF持久化功能, 服务器载入数据时使用的目标文件会有所不同:
-
如果服务器启用了AOF持久化功能, 那么服务器使用AOF文件来还原数据状态.
-
相反地, 如果服务器没有启用AOF持久化功能, 那么服务器使用RDB文件来还原数据库状态。
当服务器完成数据库状态还原工作之后, 服务器就昂在日志中打印出载入文件并还原数据库状态所耗费的时长.
执行时间循环
在初始化的最后一步, 服务器将打印出一下日志:
The server is now ready to accept connections on port 6379
并开始执行服务器的时间循环(loop).