Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转
1.命令请求的执行过程
1.1 发送命令请求
Redis服务器的命令请求来自Redis客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。
1.2 读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
1)读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面
2)对输入缓冲区中的命令请求进行解析,提取出命令请求中包含的命令参数,以及命令参数的个数,分别放在argv和argc里面
3)调用命令执行器,执行客户端指定的命令
1.3 命令执行器
1.3.1 查找命令实现
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面。
命令表是一个字典,字典的键是一个个命令名字,比如"set"、"get"、"del"等等;而字典的值则是一个个redisCommand结构,每个redisCommand结构记录了一个Redis命令的实现信息
属性名 | 类型 | 作用 |
name | char * | 命令的名字,比如"set" |
proc | redisCommandProc * | 函数指针,指向命令的实现函数 |
arity | int | 命令参数的个数,用于检查命令请求的格式是否正。 如果这个值为负数-N,那么表示参数的数量大于等于N |
sflags | char * | 字符串形式的标识值,这个值记录了命令的属性 |
flags | int | 对sflags标识进行分析得出二进制标识,由程序自动生成 |
calls | long long | 服务器总共执行了多少次这个命令 |
milliseconds | long long | 服务器执行这个命令所耗费的总时长 |
标识 | 意义 | 带有这个标识的命令 |
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 | 这是一个允许从服务器在带有过期数据时使用的命令 | SLAVEOF、PING、INFO等等 |
M | 这个命令在监视器模式下不会自动被传播 | EXEC |
注:命令名字的大小写不影响命令表的查找结果
1.3.2 执行预备操作
到目前为止,服务器已经将执行命令所需的命令实现函数、参数、参数个数都收集齐了,但在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利的执行,这些操作包括:
1)检查客户端状态的cmd指针是否执行NULL,如果是的话,返回错误
2)根据客户端cmd属性指向的redisCommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确,报错
3)检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令
4)如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,报错
5)如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgsave-error功能,而且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误
6)如果客户端当前正在用SUBSCRIBE命令订阅频道,或者正在用PSUBSCRIBE命令订阅模式,那么服务器只会执行客户端发来的SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE四个命令,其他命令都会被服务器拒绝
7)如果服务器正在进行数据库载入,那么客户端发送的命令必须带有1标识才会被服务器执行,其他命令都会被服务器拒绝
8)如果服务器因为执行Lua脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会被拒绝
9)如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令会被放进事务队列
10)如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器
1.3.3 调用命令的实现函数
在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端状态的argv属性和argc属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:
//client是指向客户端状态的指针
client->cmd->proc(client);
因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里面了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里面(buf属性和reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。
1.3.4 执行后续工作
在执行完实现函数之后,服务器还需要执行一些后续工作:
1)如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志
2)根据刚刚执行命令所耗费的时长,更新呗执行命令的redisCommand结构的milliseconds属性,并将命令的redisCommand结构的calls计数器的值增一
3)如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区
4)如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器
以上操作都执行完了,服务器对于当前命令的执行告一段落,可以执行下一个命令请求了
1.3.5 将命令回复发送给客户端
命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变成可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。
1.3.6 客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人可读的格式,并打印
1.4 serverCron函数
Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。
1.4.1 更新服务器时间缓存
Redis服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:
struct redisServer{
//...
//保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;
//保存了毫秒级精度的系统当前UNIX时间戳
long long mstime;
//...
};
因为serverCron函数默认会以每100毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度并不高:
1)服务器只会在打印日志、更新服务器的LRU时钟,决定是否执行持久化任务、计算服务器上线时间这类对时间精确度不高的功能上
2)对于为键设置过期时间、添加慢查询日志这种需要高精度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间
1.4.2 更新LRU时钟
服务器状态的lruclock属性保存了服务器的LRU时钟,这个属性和上面的unixtime属性、mstime属性一样,都是服务器时间缓存的一种:
struct redisServer{
//...
//默认每10秒更新一次的时钟缓存,用于计算键的空转时长
unsigned lruclock:22;
//...
};
每个Redis对象都有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:
typedef struct redisObject{
//...
unsigned lru:22;
//...
} robj;
当服务器要计算一个数据库键的空转时间,程序会用服务器的lruclock属性记录的时间减去对象的lru属性记录的时间,得出的计算结果就是这个对象的空转时间。
serverCron函数默认会以每10秒一次的频率更新lruclock属性的值,因为这个时钟不是实时的,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估算值。
1.4.3 更新服务器每秒执行命令次数
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;
//...
}
trackOperationsPerSecond函数每次运行,都会根据ops_sec_last_sample_time记录上一次抽样时间和服务器的当前时间,以及ops_sec_last_sample_ops记录的上一次抽样的已执行命令数量和服务器当前的已执行命令数量,计算出两次trackOperationsPerSecond调用之间,服务器平均每一毫秒处理了多少个命令请求,乘以1000得到疫苗处理的命令请求估计值,将估计值作为一个新的数组项放进ops_sec_samples环形数组里。
当客户端执行INFO命令时,服务器就会调用getOperationsPerSecond函数,根据ops_sec_samples环形数组中的抽样结果,计算出instantaneous_ops_per_sec属性的值,该值是通过计算最近REDIS_OPS_SEC_SAMPLES次取样的平均值计算得出的,是一个估算值。
1.4.4 更新服务器内存峰值记录
服务器状态中的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两个域分别以两种格式记录了服务器的内存峰值。
1.4.5 处理SIGTERM信号
在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识。
每次serverCron函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性的值决定是否关闭服务器。
1.4.6 管理客户端资源
serverCron函数每次执行都会调用clientsCron函数,该函数会对一定数量的客户端进行以下两个检查:
1)如果客户端与服务器之间的连接已经超时,就释放这个客户端
2)如果客户端在上一次执行命令请求之后,输入缓冲区的大小超过一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,从而防止耗费过多内存
1.4.7 管理数据库资源
serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中过期键,并在有需要时,对字典进行收缩操作。
1.4.8 执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后。
每次serverCron函数执行时,都会检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行。如果没有,并且aof_rewrite_scheduled属性为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令
1.4.9 持久化操作的运行状态
服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程ID,这两个属性也可以用于检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行。
每次serverCron函数执行时,程序都会检查rdb_child_pid和aof_child_pid两个属性的值,只要其中一个属性的值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:
1)如果有信号到达,那么表示新的RDB文件已经生成完毕,或者AOF文件已经重写完毕,服务器需要进行相应命令的后续操作。
2)如果没有信号到达,表示持久化未完成,等待
另一方面,如果 rdb_child_pid和aof_child_pid两个属性的值都为-1,那么表示服务器没有在进行持久化操作,这时会进行三个检查:
1)查看是否有BGREWRITEAOF被延迟了,如果有,那么开始一次新的BGREWRITEAOF
2)检查服务器的自动保存条件是否满足,如果满足,在无其他持久化操作的情况下,进行一次新的BGSAVE操作
3)检查服务器设置的AOF重写条件是否满足,如果满足,在无其他持久化操作的情况下,进行一次新的BGREWRITEAOF操作
1.4.10 将AOF缓冲区中的内容写入AOF文件
如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件里面
1.4.11 关闭异步客户端
服务器关闭那些输出缓冲区大小超出限制的客户端
1.4.12 增加cronloops计数器的值
服务器状态的cronloops属性记录了serverCron函数执行的次数。cronloops属性目前在服务器中的唯一作用,就是在复制模块中实现“每执行serverCron函数N次就执行一次指定代码”的功能
1.5 初始化服务器
1.5.1 初始化服务器状态结构
初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值:
1)设置服务器的运行ID
2)设置服务器的默认运行频率
3)设置服务器的默认文件路径
4)设置服务器的运行架构
5)设置服务器的默认端口号
6)设置服务器的默认RDB持久化条件和AOF持久化条件
7)初始化服务器的LRU时钟
8)创建命令表
initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串,除了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lua环境、共享对象这些数据结构在之后的步骤才被创建出来。
1.5.2 载入配置选项
在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。
服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置对server变量相关属性的值进行修改。
如果用户为属性设置新值就用用户设置的,否则就用默认的。
1.5.3 初始化服务器数据结构
第一步只创建了命令表一个数据结构,但是服务器还有其他数据结构,比如:
1)server.clients链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个redisClient结构实例
2)server.db数组,数组中包含了服务器的所有数据库
3)用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表
4)用于执行Lua脚本的Lua环境server.lua
5)用于保存慢查询日志的server.slowlog属性
当初时候服务器到这一步,服务器将调用initServer函数,为以上提到的数据结构分配内存,并在有需要时,为这些数据结构关联初始化值。
服务器到现在才初始化数据结构是因为,服务器必须先载入用户指定的配置选项,然后才能正确的对数据结构进行初始化。如果在执行initServerConfig函数时就对结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,就会出问题。
server状态初始化分为两步:initServerConfig负责初始化一般属性,initServer负责初始化数据结构。
除此之外,initServer还进行一些很重要的操作:
1)为服务器设置进程信号处理器
2)创建共享对象
3)打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端连接
4)为serverCron函数创建时间事件,等待服务器正式运行时执行serverCron函数
5)如果AOF持久化打开,打开现有的AOF文件,如果不存在,创建一个新的AOF,并为AOF写入做准备
6)初始化服务器的后台I/O模块,为将来的I/O做准备
1.5.4 还原数据库状态
在完成对服务器状态server变量初始化后,服务器需要载入RDB或者AOF,并根据文件记录的内容还原服务器的数据库状态。
根据服务器是否开启AOF,服务器载入数据时使用的目标文件会有所不同:
1)如果开启AOF持久化,服务器使用AOF还原数据库状态
2)否则使用RDB还原
还原好后,在日志中打印
1.5.5 执行事件循环
在初始化最后一步,打印出日志后,开始执行服务器的事件循环(loop)