Redis设计与实现 学习笔记 第十四章 服务器

时间:2024-11-01 07:42:06

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

14.1 命令请求的执行过程

如果我们使用客户端执行以下命令:
在这里插入图片描述
那么从客户端发送命令到收到回复期间,客户端和服务器需要执行以下操作:
1.客户端向服务器发送命令请求SET KEY VALUE。

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

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

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

14.1.1 发送命令请求

Redis服务器的命令请求来自Redis客户端,当用户在客户端键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器:
在这里插入图片描述
假设用户在客户端键入了命令:
在这里插入图片描述
那么客户端会将这个命令转换成协议:
在这里插入图片描述
然后将这段协议内容发送给服务器。

14.1.2 读取命令请求

当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
1.读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里。

2.对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv和argc属性里。

3.调用命令执行器,执行客户端指定的命令。

继续用上一小节的SET命令为例子,图14-2展示了服务器将命令请求保存到客户端状态的输入缓冲区后,客户端状态的样子:
在这里插入图片描述
之后,分析程序将对输入缓冲区中的以下协议内容进行分析:
在这里插入图片描述
然后将分析得出的结果保存到客户端状态的argv和argc属性里,如图14-3所示:
在这里插入图片描述
上图中有个错误,有两个argv[1],应该把后面那个改为argv[2]。

14.1.3 命令执行器(1):查找命令实现

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

命令表是一个字典,字典的键是一个个命令名字,比如“set”、“get”、“del”等;而字典的值则是一个个redisCommand结构,每个redisCommand结构记录了一个Redis命令的实现信息,表14-1记录了这个结构的主要属性的类型和作用:
在这里插入图片描述
表14-2列表了sflags属性可以使用的标识值及其意义:
在这里插入图片描述
图14-4展示了命令表的样子,并且以SET和GET命令为例子,展示了redisCommand的结构:
在这里插入图片描述
根据上图:
1.SET命令的名字为“set”,实现函数为setCommand;命令的参数个数为-3,表示命令接受三个或以上数量的参数;命令的标识为“wm”,表示SET命令是一个写入命令,且在执行这个命令前,服务器应该对内存占用情况进行检查,因为这个命令可能会占用大量内存。

2.GET命令的名字为“get”,实现函数为getCommand;命令的参数个数为2,表示命令只接受两个参数;命令的标识为“r”,表示这是一个只读命令。

继续之前SET命令的例子,当程序以图14-3中的argv[0]为输入,在命令表中进行查找时,命令表将返回“set”键对应的redisCommand结构,客户端状态的cmd指针会指向这个redisCommand结构,如图14-5所示:
在这里插入图片描述
命令名字的大小写不影响命令表的查找结果。因为命令表使用的是大小写无关的查找算法,无论输入的命令名是大写、小写、混合大小写,只要命令名是正确的,就能找到相应的redisCommand结构:
在这里插入图片描述
14.1.4 命令执行器(2):执行预备操作

到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但在真正执行命令前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:
1.检查客户端状态的cmd指针是否指向NULL,如果是,说明用于输入的命令名找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。

2.根据客户端cmd属性指向的redisCommand结构的arity属性,检查命令请求给的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。

3.检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令,如果试图执行AUTH外的命令,那么服务器将向客户端返回一个错误。

4.如果服务器打开了maxmemory功能,那么在执行命令前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返回一个错误。

5.如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了stop-writes-on-bgsave-error功能,且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误。

6.如果客户端当前正在使用SUBSCRIBE命令订阅频道,或者正在用PSUBSCRIBE命令订阅某个模式,那么服务器只会执行客户端发来的SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE命令,其他命令都会被服务器拒绝(订阅模式下,客户端的主要功能是接收和处理来自服务器推送的消息)。

7.如果服务器正在进行数据载入,那么客户端发送的命令必须带有l标识(见表14-2)才会被服务器执行,其他命令都会被服务器拒绝。

8.如果服务器因为执行Lua脚本而超时并进入阻塞状态,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会被服务器拒绝。

9.如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH命令,其他命令都会被放进事务队列中。

10.如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。

以上只列出了服务器在单机模式下执行命令时的检查操作,当服务器在复制或集群模式下执行命令时,预备操作还会更多。

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

在前面的操作中,服务器已经将要执行的命令的实现保存到了客户端状态的cmd属性里,并将命令的参数和参数个数分别保存到了客户端状态的argv和argc属性里,当服务器决定要执行命令时,它只要执行以下语句就可以了:
在这里插入图片描述
因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。

继续以之前的SET命令为例,图14-6展示了客户端包含了命令实现、参数、参数个数的样子:
在这里插入图片描述
上图中有个错误,有两个argv[1],应该把后面那个改为argv[2]。

对这个例子来说,执行语句:
在这里插入图片描述
等于执行语句:
在这里插入图片描述
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里(buf属性和reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。

对于前面的SET命令来说,函数调用setCommand(client)会产生一个“+OK\r\n”回复(这是协议格式的回复),这个回复会被保存到客户端的buf属性里,如图14-7所示:
在这里插入图片描述
14.1.6 命令执行器(4):执行后续工作

在执行完实现函数后,服务器还需要执行一些后续工作:
1.如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。

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

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

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

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

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

命令实现函数会将命令回复保存到客户端的输出缓冲区里,并为客户端的套接字关联命令回复处理器,当客户端套接字可写时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。

当命令回复发送完毕后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。

以图14-7所示的客户端状态为例,当客户端套接字可写时,命令回复处理器会将命令回复“+OK\r\n”(这是协议格式的)发送给客户端。

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

当客户端收到协议格式的命令回复后,它会将这些回复转换成人类可读的格式,并打印给客户看(假设我们使用的是Redis自带的redis-cli客户端),如图14-8所示:
在这里插入图片描述
继续以之前的SET命令为例,当客户端接到服务器发来的格式协议的回复“+OK\r\n”时,它会将这个回复转换成“OK\n”,然后打印给用户看:
在这里插入图片描述
以上就是Redis客户端和服务器执行命令请求的整个过程了。

14.2 serverCron函数

Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。

14.2.1 更新服务器时间缓存

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

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

因为serverCron函数默认每隔100毫秒执行一次,因此会以100毫秒一次的频率更新unixtime和mstime属性,所以这两个属性记录的时间的精确度并不高:
1.服务器只会在打印日志、更新服务器的LRU时钟、决定是否执行持久化任务、计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上使用unixtime和mstime属性。

2.对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间。

14.2.2 更新LRU时钟

服务器状态中的lruclock属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime和mstime属性一样,都是服务器时间缓存的一种:

struct redisServer {
    // ...
    // 默认每10秒更新一次的时钟缓存,用于计算键的空转时长(idle)
    unsigned lruclock:22;
    // ...
};

每个Redis对象都有一个lru属性,保存了对象最后一次被命令访问的时间:

typedef struct redisObject {
    // ...
    unsigned lru:22;
    // ...
} robj;

当服务器要计算一个数据库键的空转时间,程序会用服务器的lruclock属性减去对象的lru属性记录的时间,得出的就是这个对象的空转时间:
在这里插入图片描述
serverCron函数默认会以10秒一次的频率更新lruclock属性的值,因为这个时钟不是实时的,所以根据这个属性计算出来的对象空转时间实际只是一个模糊的估算值。

lruclock时钟的当前值可通过INFO server命令的lru_clock域查看:
在这里插入图片描述
14.2.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属性的值,以下是getOperationsPerSecond函数的实现代码:

long long getOperationsPerSecond(void) {
    int j;
    long long sum = 0;
    
    // 计算所有取样值的总和
    for (j = 0; j < REDIS_OPS_SEC_SAMPLES; j++)
        sum += server.ops_sec_samples[j];
    
    // 计算取样的平均值
    return sum / REDIS_OPS_SEC_SAMPLES;
}

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

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

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

struct redisServer {
    // ...
    // 已使用内存峰值
    size_t stat_peak_memory;
    // ...
};

每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量,刷新峰值记录时会更新stat_peak_memory保存的峰值。

INFO memory命令的used_memory_peak和used_memory_peak_human两个域分别以两种格式记录了服务器的内存峰值:
在这里插入图片描述
14.2.5 处理SIGTERM信号

在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识:

// SIGTERM信号的处理器
static void sigtermHandler(int sig) {
    // 打印日志
    redisLogFromHandler(REDIS_WARNING, "Received SIGTERM, scheduling shutdown...");
    // 打开关闭标识
    server.shutdown_asap = 1;
}

每次serverCron函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性值决定是否关闭服务器:

struct redisServer {
    // ...
    // 关闭服务器的标识:值为1时关闭服务器;值为0时不做动作
    int shutdown_asap;
    // ...
};

以下代码展示了服务器在接到SIGTERM信号后,关闭服务器并打印相关日志的过程:
在这里插入图片描述
从日志里可以看到,服务器在关闭自身前会进行RDB持久化操作,这也是服务器拦截SIGTERM信号的原因,如果服务器一接到SIGTERM信号就立即关闭,那么就没办法执行持久化操作了。

14.2.6 管理客户端资源

serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会对一定数量的客户端进行以下检查:
1.如果客户端与服务器之间的连接已经超时(即空闲时长过长),那么程序释放这个客户端。

2.如果客户端上次执行的命令请求过长,导致输入缓冲区的大小超过了一定的长度,那么程序会释放客户端当前的输入缓冲区,并重新创建一个默认大小的缓冲区,从而防止客户端的输入缓冲区耗费过多内存。

14.2.7 管理数据库资源

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

14.2.8 执行被延迟的BGREWRITEAOF

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

服务器的aof_rewrite_scheduled标识记录了服务器是否延时了BGREWRITEAOF命令:

struct redisServer {
    // ...
    // 如果值为1,那么表示有BGREWRITEAOF命令被延迟了
    int aof_rewrite_scheduled;
    // ...
};

每次serverCron函数执行时,函数都会检查BGSAVE命令或BGREWRITEAOF命令是否正在执行,如果这两个命令都没在执行,且aof_rewrite_scheduled属性值为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令。

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

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

struct redisServer {
    // ...
    // 记录执行BGSAVE命令的子进程ID,如果当前没有执行BGSAVE命令,则此值为-1
    pid_t rdb_child_pid;    /* PID of RDB saving child */
    // 记录执行BGREWRITEAOF命令的子进程ID,如果当前没有执行BGREWRITEAOF命令,则此值为-1
    pid_t aof_child_pid;    /* PID if rewriting process */
    // ...
};

每次serverCron函数执行时,程序都会检查rdb_child_pid和aof_child_pid两个属性值,只要其中一个属性值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发给服务器进程:
1.如果有信号到达,则表示新的RDB文件已经生成完毕(对于BGSAVE命令来说),或AOF文件已经重写完毕(对于BGREWRITEAOF命令来说),服务器需要进行相应命令的后续操作,比如用新的RDB文件替换现有RDB文件,或用重写后的AOF文件替换现有AOF文件。

2.如果没有信号到达,那么说明持久化操作未完成,程序不做动作。

另一方面,如果rdb_child_pid和aof_child_pid属性值都为-1,那么表示服务器没有在进行持久化操作,此时,程序执行以下检查:
1.查看是否有BGREWRITEAOF被延迟了,如果有的话,开始一次新的REWRITEAOF操作。

2.检查服务器的自动保存条件是否已满足,如果条件满足,且服务器没有在执行其他持久化操作,那么服务器开始一次BGSAVE操作(虽然2是在rdb_child_pid和aof_child_pid属性都为-1时进行的检查,但由于1的存在,服务器还是会检查一下是否正在进行持久化操作)。

3.检查服务器设置的AOF重写条件是否满足,如果条件满足,且服务器没有在执行其他持久化操作,那么服务器将开始一次新的BGREWRITEAOF操作(虽然3是在rdb_child_pid和aof_child_pid属性都为-1时进行的检查,但由于1和2的存在,服务器还是会检查一下是否正在进行持久化操作)。

图14-9以流程图的方式展示了这个检查过程:
在这里插入图片描述
14.2.10 将AOF缓冲区中的内容写入AOF文件

如果服务器开启了AOF持久化功能,且AOF缓冲区里还有待写入的数据,那么serverCron函数会调用相应程序,将AOF缓冲区中的内容写入AOF文件里,第十一章对此有详细说明。

14.2.11 关闭异步客户端

在这一步,服务器会关闭那些输出缓冲区大小超出限制的客户端,第十三章对此有详细说明。

14.2.12 增加cronloops计数器的值

服务器状态的cronloops属性记录了serverCron函数执行的次数:

struct redisServer {
    // ...
    // serverCron函数的运行次数计数器
    // serverCron函数每执行一次,这个属性的值就增一
    int cronloops;
    // ...
};

cronloops属性目前在服务器中的唯一作用,就是在复制模块中实现“每执行serverCron函数N次就执行一次指定代码”的功能,方法如以下伪代码所示:

if cronloops % N == 0:
    # 执行指定代码...

14.3 初始化服务器

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

14.3.1 初始化服务器状态结构

初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。

初始化server变量的工作由redis.c/initServerConfig函数完成,以下是这个函数最开头的一部分代码:

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) == 64) ? 64 : 32;
    // 设置默认服务器端口号
    server.port = REDIS_SERVERPORT;
    // ...
}

以下是initServerConfig函数完成的主要工作:
1.设置服务器的运行ID。

2.设置服务器的默认运行频率。

3.设置服务器的默认配置文件路径。

4.设置服务器的运行架构。

5.设置服务器的默认端口号。

6.设置服务器的默认RDB持久化条件和AOF持久化条件。

7.初始化服务器的LRU时钟。

8.创建命令表。

initServerConfig函数设置的服务器状态属性的类型基本都是整数、浮点数、字符串(除了字典类型的命令表),数据库、慢查询日志、Lua环境、共享对象这些数据结构在之后的步骤才会被创建出来。

当initServerConfig函数执行完后,服务器就进入初始化的第二个阶段——载入配置选项。

14.3.2 载入配置选项

启动服务器时,用户可以通过给定配置参数或指定配置文件来修改服务器的默认配置。例如,如果我们在终端输入:
在这里插入图片描述
那么我们就通过给定配置参数的方式,修改了服务器的运行端口号。又例如,我们在终端中输入:
在这里插入图片描述
且redis.conf文件中包含以下内容:
在这里插入图片描述
那么我们就通过指定配置文件的方式修改了服务器的数据库数量,以及RDB持久化模块的压缩功能开关。

服务器在用initServerConfig函数初始化完server变量后,就会载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性进行修改。

例如,初始化server变量时,程序先会为服务器端口号设置默认值:

void initServerConfig(void) {
    // ...
    // 默认值为6379
    server.port = REDIS_SERVERPORT;
    // ....
}

但如果用户启动服务器时指定了端口号,那么随后port属性会被更新为用户指定的值。

同理,如果我们在服务器启动时指定了数据库数量,那么最后服务器的数据库数量就会从默认的16更新为用户指定的值。

服务器载入了用户指定的选项,并对server状态更新后,就可以进入初始化的第三个阶段——初始化服务器数据结构。

14.3.3 初始化服务器数据结构

之前执行的initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构,除了命令表外,服务器还包含其他数据结构:
1.server.clients链表,其中记录了所有与服务器相连的客户端状态结构,链表的每个节点都包含一个redisClient结构实例。

2.server.db数组,数组中包含了服务器的所有数据库。

3.用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。

4.用于执行Lua脚本的Lua环境server.lua。

5.用于保存慢查询日志的server.slowlog属性。

服务器会调用initServer函数,为以上数据结构分配内存,有需要时,还会为这些数据结构设置或关联初始化值。

服务器现在才初始化数据结构的原因在于,必须先载入用户指定的配置选项,然后才能正确地初始化数据结构。如果执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户指定了和数据结构有关的配置选项,服务器就要重新调整和修改已创建的数据结构。

除了初始化数据结构外,initServer还进行了以下操作:
1.为服务器设置进程信号处理器。

2.创建共享对象:这些对象包含Redis服务器经常用到的一些值,比如包含“OK”、“ERR”的字符串对象(用于回复客户端),从1到10000整数值的字符串对象等,服务器通过重用这些共享对象来避免反复创建相同对象。

3.打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等服务器正式运行时,用来接受客户端的连接。

4.为serverCron函数创建时间事件,等服务器正式运行时,执行serverCron函数。

5.如果AOF持久化功能已打开,那么打开现有AOF文件,如果AOF文件不存在,则创建并打开一个新AOF文件,为AOF写入做好准备。

6.初始化服务器后台的IO模块(bio),为将来的IO操作做好准备。

当initServer函数执行完后,服务器将用ASCII字符在日志中打印Redis的图标以及版本号信息:
在这里插入图片描述
14.3.4 还原数据库状态

完成了对服务器状态server变量的初始化后,服务器需要载入RDB文件或AOF文件来还原服务器的数据库状态。

根据服务器是否启用了AOF持久化功能,服务器载入数据时使用的文件也会不同:
1.如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态。

2.如果服务器没有启用AOF持久化功能,则使用RDB文件来还原数据库状态。

当服务器完成数据库状态还原工作后,服务器将在日志中打印出时长:
在这里插入图片描述
14.3.5 执行事件循环

在初始化的最后一步,服务器将打印出以下日志:
在这里插入图片描述
并开始执行服务器的事件循环。

至此,服务器的初始化工作完成,可以开始接受客户端的连接请求。

14.4 重点回顾

1.一个命令请求从发送到完成主要包括:
(1)客户端将命令请求发送给服务器;

(2)服务器读取命令请求,并分析出命令参数;

(3)命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复;

(4)服务器将命令回复返回给客户端。

2.serverCron函数默认每100毫秒执行一次,它的主要工作包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等。

3.服务器从启动到能处理客户端的命令请求需要以下步骤:
(1)初始化服务器状态;

(2)载入服务器配置;

(3)初始化服务器数据结构;

(4)还原数据库状态;

(5)执行事件循环。