0%

Redis 设计与实现-服务器

Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。

1. 命令请求的执行过程

一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。举个例于,如果我们使用客户端执行以下命令:

1
2
redis> SET KEY VRLUE
OK

那么从客户端发送 SET KEY VRLUE 命令到获得回复OK期间,客户端和服务器共需要执行以下操作:

  1. 客户端向服务器发送命令请求SET KEY VRLUE

  2. 服务器接收并处理客户端发来的命令请求 SET KEY VRLUE ,在数据库中进行设置操作,并产生命令回复OK。

  3. 服务器将命令回复OK发送给客户端。

  4. 客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。

1.1 发送命令请求

Redis服务器的命令请求来自Redis客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器,如图14-1所示。

1.2 读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

  1. 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
  2. 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性里面。
  3. 调用命令执行器,执行客户端指定的命令。

1.3 命令执行器-查找命令实现

命令执行器要做的第一件事就是根据客户端状态的argv[01]参数,在命令表(command table)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面。

命令表是一个字典,字典的键是一个个命令名字,比如“set“、ˇget“、“ˇdel“等等; 而字典的值则是一个个 redisCommand 结构,每个 redisCommand 结构记录了一个 redis 命令的实现信息。示例图如下:

1.4 命令执行器-执行预备操作

在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:

  • 检查客户端状态的 cmd 指针是否指向NULL,如果是的话,那么说明用户输入的命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个
    错误。
  • 根据客户端cmd属性指向的redisCommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤、直接向客户端返回一个错误。比如说,如果redisCommand结构的axity属性的值为-3,那么用户输入的命令参数个数必须大于等于3个才行。
  • 检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行 AUTH 命令,如果未通过身份验证的客户端试图执行除AUTH 命令之外的其他命令,那么服务器将向客户端返回一个错误。

1.5 命令执行器-调用命令的实现函数

在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的 cmd 属性里面,并将命令的参数和参数个数分别保存到了客户端状态的argv属性和argv属性里面,当服务器要执行命令时,它只要执行以下语句就可以了:

1
2
// client 是指向客户端状态的指针
client -> cmd -> proc(client)

1.6 命令执行器-执行后续工作

在执行完实现函数之后,服务器还需要执行一些后续工作:

  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。

  • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的 milliseconds 属性….并将命令的 redisCommand 结构的 calls 计数器的值增一。

  • 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写人到AOF缓冲区里面。

  • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。

当以上操作都执行完丁之后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。

1.7 将命令回复发送给客户端

命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。

1.8 客户端接收并打印命令回复

当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看。

2. serverCorn 函数

2.1 更新服务器时间缓存

Redis服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:

1
2
3
4
5
6
7
struct redisServer {
// ...
// 保存了秒级精度的系统当前 UNIX 时间戳
time_t unixtime;
// 保存了毫秒精度的系统当前 UNIX 时间戳
long long mstime;
};

因为serverCron函数默认会以每100毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度并不高:

  • 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上。
  • 对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间。

2.2 更新 LRU 时钟

服务器状态中的Lruclock属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime属性、mstime属性一样,都是服务器时间缓存的一种。serverCron 函数默认会以每10秒一次的频率更新Lruclock属性的值.因为这个时钟不是实时的,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估算值。

2.3 更新服务器每秒执行命令次数

serverCron函数中的 trackOperationsPerSecond 函数会以每100毫秒一次的频率执行,这个函数的功能是以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量。

2.4 更新服务器内存峰值记录

服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小:

1
2
3
4
5
6
struct redisServer {
// ...
// 已使用内存峰值
size_t stat_peak_memory;
// ...
};

每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量,并与stat_peak_memory保存的数值进行比较,如果当前使用的内存数量比stat_peak_memory 属性记录的值大,那么程序就将当前使用的内存数量记录到stat_peak_memory属性里面。

2.5 处理 SIGTERM 信号

在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识,并根据属性的值决定是否关闭服务器。

2.6 管理客户端资源

serverCron 函数每次执行都会调用 clientsCron 函数,clientsCorn 函数会对一定数量的客户端进行以下两个检查:

  • 如果客户端与服务器之间的连接已经超时(很长一段时间里客户端和服务器都没有互动),那么程序释放这个客户端。
  • 如果客户端在上一次执行命令请求之后,输人缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输人缓冲区,并重新创建一个默认大小的输人缓冲区,从而防止客户端的输人缓冲区耗费了过多的内存。

2.7 管理数据库资源

函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作。

2.8 执行被延迟的 BGREWRITEAOF

在服务器执行 BGSAVE 命令的期间,如果客户端向服务器发来 BGREWRITEAOF 命令后,那么服务器会将 BGREWRITEAOF 命令的执行时间延迟到BGSAVE 命令执行完毕之后。

2.9 检查持久化操作的运行状态

服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行 BGSAVE 命令和 BGREWRITEAOF 命令的子进程的ID,这两个属性也可以用于检查 BGSAVE 命令或者BGREWRITEAOF 命令是否正在执行。

2.10 将 AOF 缓冲区中的内容写入 AOF 文件

如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件里面。

2.11 关闭异步客户端

在这一步,服务器会关闭那些输出缓冲区大小超出限制的客户端。

2.12 增加 cronloops 计数器的值

服务器状态的cronloops属性记录了serverCron函数执行的次数。每执行一次 serverCron 函数计数器值增一。

3. 初始化服务器

3.1 初始化服务器状态结构

初始化服务器的第一步就是创建一个struct redisServer类型的实例变量 server 作为服务器的状态,并 为结构中的各个属性设置默认值。初始化server变量的工作由redis.c/initServerconfig 函数完成,

3.2 载入配置选项

在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。

3.3 初始化服务器数据结构

在之前执行initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构,不过除了命令表之外,服务器状态还包含其他数据结构,比如:

  • server.clients 链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个redisClient结构实例。
  • server.db数组,数组中包含了服务器的所有数据库。
  • 用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
  • 用于执行Lua脚本的Lua环境server.lua。
  • 用于保存慢查询日志的server.slowlog属性。
  • 当初始化服务器进行到这一步,服务器将调用initServer丽数,为以上提到的数据结构分配内存,并在有需要时,为这些数据结构设置或者关联初始化值。

3.4 还原数据库状态

在完成了对服务器状态server变量的初始化之后,服务器需要载人RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。

3.5 执行时间循环

初始化最后一步,开始执行服务器的事件循环。

------ 本文结束------