redis设计与实现(14)服务器

来源:互联网 发布:淘宝客返利在哪里查看 编辑:程序博客网 时间:2024/06/15 11:13

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

命令请求的执行过程

一个命令请求从发送到获得回复的过程中,客户端和服务器需要完成一系列操作。举个例子,如果我们使用客户端执行以下命令:
redis>SET KEY VALUE
OK
那么客户端发送SET KEY VALUE命令到获得回复OK期间,客户端和服务器需要执行以下操作:
  1. 客户端向服务器发送命令请求SET KEY VALUE。
  2. 服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK。
  3. 服务器将命令回复OKI发送给客户端。
  4. 客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。

发送命令请求

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

之后,分析程序将对输入缓冲区中的协议进行分析:

struct redisCommand {    // 命令名字    char *name;    // 实现函数    redisCommandProc *proc;    // 参数个数    int arity;    // 字符串表示的 FLAG    char *sflags; /* Flags as string representation, one char per flag. */    // 实际 FLAG    int flags;    /* The actual flags, obtained from the 'sflags' field. */    /* Use a function to determine keys arguments in a command line.     * Used for Redis Cluster redirect. */    // 从命令中判断命令的键参数。在 Redis 集群转向时使用。    redisGetKeysProc *getkeys_proc;    /* What keys should be loaded in background when calling this command? */    // 指定哪些参数是 key    int firstkey; /* The first argument that's a key (0 = no keys) */    int lastkey;  /* The last argument that's a key */    int keystep;  /* The step between first and last key */    // 统计信息    // microseconds 记录了命令执行耗费的总毫微秒数    // calls 是命令被执行的总次数    long long microseconds, calls;};
下表列出了sflags属性可以使用的标识值,以及这些标识值的意义。

命令执行器(2):执行预备操作

到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但是在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括
  • 检查客户端状态的cmd指针是否指向NULL,如果是的话,那么说明用户输入的命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。
  • 根据客户端cmd属性指向的redisCommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。比如说,如果redisCommand结构的arity属性的值为-3,俺么用户输入的命令参数个数必须大于等于3个才行。
  • 检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令,如果未通过身份验证的客户端试图执行出AUTH命令之外的其他命令,那么服务器将向客户端返回一个错误。
  • 如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返回一个错误。
  • 如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgsave-error,而且服务器将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误。
  • 如果客户端当前正在用SUBSCRIBE命令订阅频道,或者正在用PSUBSCRIBE命令订阅模式,那么服务器只会执行客户端发来的SUBSCRIBE、PSUBSRIBE、UNSUBSCRIBE、PUBSUBSCRIBE四个命令,其他命令都会被服务器拒绝。
  • 如果服务器正在进行数据载入、那么客户端发送的命令必须带有1标识(比如INFO、SHUTDOWN、PUBLISH等等)才会被服务器执行,其他命令都会被服务i期拒绝。
  • 如果服务器因为执行Lua脚本而超时并进行阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会被服务器拒绝。
  • 如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令都会被放进事务队列中。
  • 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。当完成了以上预备操作之后,服务器可以开始真正执行命令了。
注意:以上只列出了服务器在单击模式下执行命令时的检查操作,当服务器在复制或者集群模式下执行命令时,预备操作还会更多一些。


命令执行器(3):调用命令的实现函数

在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端状态的srgv属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:
//client是指向客户端状态的指针
client->cmd->proc(client);
因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里面了,所以命令的实现函数只需要一个执行客户端状态的指针作为参数即可。
继续以之前的SET命令为例子,图14-6展示了客户端包含了命令实现,参数和参数个数的样子。
对于这个例子来说,执行语句:
client->cmd->proc(client);
等于执行语句:
setCommand(client);

命令执行器(4):执行后续工作

在执行完实现函数之后,服务器还需要执行一些后续工作:
  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds属性,并将命令的redisCommand结构的calls计数器的值增一。
  • 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面。
  • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。
当以上操作都执行完了以后,服务器对于当前命令的执行到此就告一段落了,之后服务器就可以继续从文件事件处理器取出并处理下一个命令请求了。

将命令回复发送给客户端

前面说过,命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。
以图14-7所示的客户端状态为例子,当客户端的套接字变为可写状态时,命令回复处理器会将协议格式的命令回复“+OK\r\n”发送给客户端。



更新服务器时间缓存

Redis服务器中有不少功能都需要获取系统的当前时间,而每次获取系统当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:
struct redisServer{
// ...
//保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;

//保存了毫秒级精度的系统当前UNIX时间戳
long long mstime;
  //...
}
因为severCron函数默认会以每100毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度不高:
  • 服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上。
  • 对于为键试着过期时间、添加慢查询日志这种需要高精确度事件的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间。





根据getOperationsPerSecond函数的定义可以看出,instantaneous_ops_per_sec属性的值是通过计算最近REDIS_OPS_SEC_SAMPLES次取样的平均值来计算得出的,它只是一个估算值。

更新服务器内存峰值记录

服务器状态中的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两个域分别以两种格式记录了服务器的内存峰值:
redis>INFO memory
#Memory
...
used_momory_peak:501824
used_memory_peak_human:490.06K
...

处理SIGTERM信号

在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识:
//SIGTERM信号的处理器
static void sigtermHandler(int sig){
//打印日至
redisLogFromHandler(REDIS_WARNIING,"Received SIGTERM,scheduling shutdown...");
//打开关闭标识
server.shutdown_asap = 1;
}
每次serverCron函数运行时,程序都会对服务器状态的shutdown_asp属性进行检查,并根据属性的值决定是否关闭服务器:
struct redisServer{
//...
  //关闭服务器的标识:
//值为1时,关闭服务器,
//值为0时,不做动作。
int shutdown_asap;
//...
};

执行被延迟的BGREWRITEAOF

在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后。
服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF命令:
struct redisServer{
//...
//如果值为1,那么表示有BGREWRITEAOF命令被延迟了。
int aof_rewrite_scheduled;
};

  • 如果有信号到达,那么表示新的RDB文件已经生成完毕(对于BGSAVE命令来说)或者AOF文件已经重写完毕(对于BGREWRITEAOF命令来说),服务器需要进行相应命令的后续操作,比如用新的RDB文件替换现有的RDB文件,或者用重写后的AOF文件替换现有的AOF文件。
  • 如果没有信号到达,那么表示持久化操作未完成,程序不做动作。
另一方面,如果rdb_child_pid和aof_child_pid两个属性的值都为-1,那么表示服务器没有在进行持久化操作,在这种情况下,程序执行以下三个检查:
  1. 查看是否有BGREWRITEAOF被延迟了,如果有的话,那么开始一次新的BGREWRITEAOF操作(这就是上一个小节我们说到的检查)。
  2. 检查服务器自动保存调节事发后已经被满足,如果条件满足,并且服务器没有在执行其他持久化操作,那么服务器开始一次新的BGSAVE操作(因为条件1可能会引发一次BGREWRITEAOF,所以在这个检查中,程序会再次确认服务器是否已经在执行持久化操作了)。
  3. 检查服务器设置的AOF重写条件是否满足,如果条件满足,并且服务器没有在执行其他持久化操作,那么服务器将开始一次新的BGREWRITEAOF操作(因为条件1和条件2都可能会以你期新持久化操作,所以在这个检查中,我们要再次确认服务器是否已经在执行持久化操作了)。
cronloops属性目前在服务器中的唯一作用,就是在复制模块中实现“每执行serverCron函数N次就执行一次指定代码”的功能,方法如以下伪代码所示:
if cronloops % N == 0;
#执行指定代码...

初始化服务器

一个redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等等,本节接下来的内容将对服务器的整个初始化过程进行详细的介绍。

初始化服务器状态结构

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

以下是initServerConfig函数完成的主要工作:
  • 设置服务器的运行ID
  • 设置服务器的默认运行频率
  • 设置服务器的默认配置文件路径
  • 设置服务器的运行架构
  • 设置服务器的默认端口号
  • 设置服务器的默认RDB持久化条件和AOF持久化条件
  • 初始化服务器的LRU时钟
  • 创建命令表
initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表之外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、满查询日志、Lua环境、共享对象这些数据结构在之后的步骤才会被创建出来。
当initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段——载入配置选项。

载入配置选项

在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。举个例子,如果我们在终端中输入:
$ redis-server --port 10086
那么我们就通过给定配置参数的方式,修改了服务器的运行端口号。另外,如果我们在终端中输入:
$ redis-server redis.conf
并且redis.conf文件中包含以下内容:
#将服务器的数据库数量设置为32个
databases 32
#关闭RDB文件的压缩功能
rebcompression no
那么我们就通过指定配置文件的方式修改了服务器的数据库数量,以及RDB持久化模块的压缩功能。
服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。
例如,在初始化server变量时,程序会决定为服务器端口号port属性设置默认值:
void initServerConfig(void){
// ...
//默认值为6379
server.port = REDIS_SERVERPORT;

// ...
}
不过,如果用户在启动服务器时为配置选项port指定了新值10086,那么server.port属性的值就会被更新为10086,这将使得服务器的端口号从默认的6379变为用户指定的10086。
例如,在初始化server变量时,程序会为决定数据库数量的dbnum属性设置默认值:
void initServerConfig(void){
// ...
//默认值为16
server.dbnum = REDIS_DEFAULT_DBNUM;
// ...
}

初始化服务器数据结构

在之前执行initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构,不过除了命令表之外,服务器状态还包含其他数据结构,比如:
  • server.clients链表,这个链表记录了所有与服务器相连的客户端的状态结构,链表的每个节点都包含了一个redisClient结构实例。
  • server.db数组,数组中包含了服务器的所有数据库。
  • 用于保存频道定义信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
  • 用于执行Lua脚本的Lua环境server.lua。
  • 用于保存慢查询日志的server.slowlog属性。
当初始化服务器进行到这一步,服务器将调用initServer函数,为以上提到的数据结构分配内存,并在有需要时,为这些数据结构设置或者关联初始化值。
服务器到现在才初始化数据结构的原因在于,服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化,如果在执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构。为了避免出现这种麻烦的情况,服务器选择了将server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而initServer函数主要负责初始化数据结构。
除了初始化数据结构之外,initServer还进行了一些非常重要的设置操作,其中包括:
  • 为服务器设置进程信号处理器。
  • 创建共享对象:这些对象包含Redis服务器经常用到的一些值,比如包含"OK"回复的字符串对象,包含“ERR”回复的字符串对象,包含整数1到10000的字符串对象等等,服务器通过重用这些对象来避免反复创建相同的对象。
  • 打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接。
  • 为serverCron创建时间事件,等待服务器正式运行时执行serverCron函数。
  • 如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备。
  • 初始化服务器的后台I/O模块(bio),为将来的I/O操作做好准备。
当initServer函数执行完毕之后,服务器将用ASCII字符在日志中打印出Redis的图标,以及Redsi的版本号信息:

还原数据库状态

在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。
根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同:
  • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态。
  • 相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态。
当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件爱呢并还原数据库状态所耗费的时长:

原创粉丝点击