Redis笔记(12):服务端实现

本文详细介绍了Redis服务器处理命令请求的过程,包括客户端发送命令、服务器读取与执行、命令执行器的工作,以及资源管理的多个方面,如时间缓存、LRU时钟更新、内存管理和信号处理。此外,还阐述了`serverCron`函数在维护服务器状态、执行延迟任务和管理资源中的关键作用。
摘要由CSDN通过智能技术生成

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

1.命令请求的执行过程

(1)发送命令请求

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

img

(2)读取命令请求

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

1)读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。

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

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

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

命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表中查找参数所指定的命令,命令名称可忽略大小写,并将找到的命令保存到客户端状态的cmd属性里面。命令表是一个字典,字典的键是命令名字,比如"SET"、“GET”、"DEL"等等;字典的值则是一个redisCommand结构,每个redisCommand结构记录了一个Redis命令的实现信息。

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

到此,服务器已经将执行命令所需的命令实现函数、参数、参数个数都收集齐了,在真正执行命令之前,程序还需要进行一些预备操作,以确保命令可以正确、顺利地被执行,部分操作包括:

  • 检查客户端状态的cmd指针是否指向NULL,如果是的话,那么说明用户输入的命令名字找不到相应的命令实现,服务器不再执行后续步骤,并向客户端返回一个错误。
  • 根据客户端cmd属性指向的redisCommand结构的arity属性,检査命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤,直接向客户端返回一个错误。
  • 检查客户端是否已经通过了身份验证,未通过身份验证的客户端只能执行AUTH命令,如果未通过身份验证的客户端试图执行除AUTH命令之外的其他命令,那么服务器将向客户端返回一个错误。
  • 如果服务器打开了maxmemory功能,在执行命令之前,先检査服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。如果内存回收失败,那么不再执行后续步骤,向客户端返回一个错误。
  • 如果服务器上一次执行BGSAVE命令时出错,并且服务器打开了stop-writeson-bgsave-error功能,而且服务器即将要执行的命令是一个写命令,那么服务器将拒绝执行这个命令,并向客户端返回一个错误。

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

当服务器决定要执行命令时,它只要执行以下语句就可以了:client -> cmd -> proc(client);因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里面了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。

被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区里面(buf属性和reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。

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

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

  • 如果服务器开启了慢査询日志功能,那么慢査询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds属性,并将calls属性的值加一。
  • 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面。
  • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。

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

(7)将命令回复发送给客户端

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

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

(8)客户端接收并打印命令回复

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

2.serverCron函数

Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。serverCron函数会更新redisServer的部分属性值,执行的操作如下。

struct redisServer{
    // 保存了秒级精度的系统当前UNIX时间戳
    time_t unixtime;
    // 保存了毫秒级精度的系统当前UNIX时间戳
    long long_mstime;
    // 默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长
    unsigned lruclock:22;
    // 已使用内存峰值
    size_t stat_peak_memory;
    // 关闭服务器的标识,值为1,关闭服务器,值为0,不做动作
    int shutdown_asap;
    // 如果值为l,那么表示有BGREWRITEAOF命令被延迟了
    int aof_rewrite_scheduled;
    // 记录执行BGREWRITEAOF命令的子进程的ID,未执行,则值为-1
    pid_t aof_child_pid;
    // 记录执行BGSAVE命令的子进程的ID,未执行,则值为-1
    pid_t rdb_child_pid;
    // serverCron函数的运行次数计数器,每执行一次,值就增一
    int cronloops;
};

(1)更新时间缓存

Redis服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存。serverCron函数默认会以每100毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度并不高。

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

(2)更新LRU时钟

服务器状态中的lruclock属性保存了服务器的LRU时钟,每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间。当服务器要计算一个数据库键的空转时间,程序会用服务器的lruclock属性记录的时间,减去对象的lru属性记录的时间,得出的计算结果就是这个对象的空转时间。servercron函数默认会以每10秒一次的频率更新lruclock属性的值,因为这个时钟不是实时的,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估算值。

(3)更新服务器每秒执行命令次数

servercron函数中的trackOperationsPerSecond函数会以每100毫秒一次的频率执行,以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过INFO status命令的 instantaneous_ops_per_sec域查看。

(4)更新服务器内存峰值记录

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

(5)拦截处理SIGTERM信号

在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号,将服务器状态的shutdown_asap标识设置为1,每次 serverCron函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性的值决定是否关闭服务器。服务器在关闭自身之前会进行RDB持久化操作,这也是服务器拦截SIGTERM信号的原因,如果服务器一接到SIGTERM信号就立即关闭,那么它就没办法执行持久化操作了。

(6)管理客户端资源

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

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

(7)管理数据库资源

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

(8)执行被延迟的BGREWRTEAOF

在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,服务器会将BGREWRITEAOF命令的执行时间延迟到BGSAVE命令执行完毕之后。redisServer的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF命令。

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

(9)检查持久化操作的运行状态

服务器状态使用rdb_child_pid、aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程的ID,这两个属性也可以用于检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行。每次serverCron函数执行时,程序都会检查rdb_child_pid、aof_child_pid两个属性的值,只要其中一个属性的值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:

  • 如果信号到达,表示持久化操作已经完成,使用新的RDB、AOF文件替换原有的文件。
  • 如果信号未到达,表示持久化操作还在继续执行,不做任何操作。

(10)将AOF缓冲区中的内容写入AOF文件

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

(11)增加cronloops计数器的值

redisServer的cronloops属性记录了servercron函数执行的次数。

3.初始化服务器

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

(1)初始化服务器状态结构

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

(2)载入配置选项

在启动服务器时,可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。

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

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

  • server.clients链表,记录了所有与服务器相连的客户端的redisClient。
  • server.db数组,数组中包含了服务器的所有数据库。
  • 保存频道订阅/模式信息的server.pubsub_channels字典/server.pubsub_patterns链表。
  • 用于执行Lua脚本的Lua环境server.lua。
  • 用于保存慢查询日志的server.slowlog属性。

当初始化服务器进行到这一步,服务器将调用nitServer函数,为上述数据结构分配内存,并在有需要时,为这些数据结构设置或者关联初始化值。服务器到现在才初始化数据结构的原因在于,服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。如果在执行 initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置选项修改了和数据结构有关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构。为了避免岀现这种麻烦的情况,服务器选择了将server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而initServer函数主要负责初始化数据结构。

除了初始化数据结构之外,initserver还进行了一些非常重要的设置操作,其中包括:

  • 为服务器设置进程信号处理器。
  • 创建共享对象:包括Redis服务器经常用到的一些值,"OK"字符串对象,"ERR"字符串对象,整数1到10000的字符串对象,服通过重用这些共享对象来避免反复创建相同的对象。
  • 打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接。
  • 为Servercron函数创建时间事件,等待服务器正式运行时执行Servercron函数。
  • 如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在那么创建并打开一个新的AOF文件,为AOF写入做好准备。
  • 初始化服务器的后台IO模块(bio),为将来的IO操作做好准备。

当initServer函数执行完毕之后,服务器将用ASCI字符在日志中打印出Redis的图标。

(4)还原数据库状态

在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。根据服务器是否启用了AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同:

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

当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长。

(5)执行事件循环

在初始化的最后一步,服务器将打印出以下日志:“The server is now ready to accept connections on port 6379”,并开始执行服务器的事件循环,至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值