14.1 命令请求的执行过程
14.1.1 发送命令请求
客户端将用户输入的命令转化成协议格式,连接到服务器的客户端套接字,将命令请求发送给服务器
14.1.2 读取命令请求
当客户端套接字因为客户端的写入而可读时,产生文字事件,服务器调用命令处理器处理
- 读取套接字中的命令,存储到客户端状态输入缓冲区
- 解析输入缓冲区的命令,提取命令名和参数赋值到argv属性和argc属性
- 根据执行的命令寻找对应的命令实现函数执行命令
14.1.3 命令执行器(1):查找命令实现
命令执行器根据argv[0]参数,在命令表中找到参数对应的命令,将客户端的cmd属性指向找到的命令(redisCommand结构)
命令表是一个字典,键是命令名,值是redisCommand结构,每个redisCommand记录了命令的实现信息,其主要属性:
- name(char *):命令的名字
- proc(redisCommandProc *):函数指针,指向实现的函数
- arity(int):命令参数的个数,用于校验命令请求格式。如果值是-N,代表参数个数大于等于N,参数个数包含了命令名
- sflags(char *):记录了命令的属性,比如是写命令还是读命令等等
- flags:对sflags分析得出的二进制标识,程序检测时使用flags而不是sflags,因为可以使用各种二进制操作
- calls:服务器总共执行了多少次命令
- milliseconds:服务器执行命令的总耗时
14.1.4 命令执行器(2):执行预备操作
进行一些预备操作,确保命令被正确、顺利的执行,
- 检查命令的cmd指针是否为空,若为空代表找不到合适的命令,向客户端返回一个错误
- 根据redisCommand的arity属性,检查持有的入参是否符合方法的要求,若不符合返回一个错误
- 检查客户端是否通过了身份验证
- 如果服务器打开了maxmemory功能,服务器在执行命令前会检查内存是否足够,必要时进行内存回收,如果内存回收失败,将返回一个错误
- ...
14.1.5 命令执行器(3):调用命令的实现函数
调用命令执行函数,将一个指向客户端状态的指针作为入参传入命令执行函数即可,因为客户端状态的属性中已经包含了需要的入参。
命令执行函数执行完毕,产生回复后,回复会被保存到客户端状态的输出缓冲区。之后命令实现函数会将客户端套接字的可写事件关联到命令回复处理器
14.1.6 命令执行器(4):执行后续工作
在命令执行函数执行完毕后,服务器还需要执行一些后续工作
- 如果开启了慢查询日志,慢查询日志会检查,是否需要为刚刚的操作添加慢查询日志
- 将这次执行的耗时增加到redisCommand.milliseconds属性上,并更新calls属性
- 如果开启了AOF持久化功能,将上一次执行的命令写入到AOF缓冲区
- 如果有从服务器,当前服务器会将命令传播给所有从服务器
当上述操作都完成后,一个完整的命令的执行完毕,服务器将会从文件事件处理器中取出下一个命令执行
14.1.7 将命令回复发送给客户端
当客户端查询返回结果时,客户端套接字产生可写事件,命令回复处理器将输出缓冲区的回复写入到套接字,当命令回复发送完毕之后,回复处理器会清空客户端的输出缓冲区,为处理下一个请求做好准备。
14.1.8 客户端接收并打印命令回复
当客户端接收到协议格式的回复时,会将其转化成可读格式,并打印给用户
注意:14.1.5先将回复写入到客户端缓冲区,然后14.1.6才会进行AOF持久化和发送从服务,极端情况下,客户端读取到返回,但是服务器还未持久化或从服务器尚未同步,此时数据丢失。
14.2 serverCron函数
serverCron函数默认1秒执行10次,检查服务器资源,保持服务器良好运转
14.2.1 更新服务器时间缓存
Redis获取系统的当前时间,需要进行系统调用,为了减少系统调用次数,服务器状态保存了两个近似的时间属性
struct redisServer{ //保存了秒级精度的系统当前UNIX时间戳 time_t unixtime; //保存了毫秒级精度的系统当前UNIX时间戳 long long mstime; // ... }
serverCron函数每100毫秒更新一次这两个属性,在对时间精度要求不高的情况下,使用这两个时间。而对于类似替键设置过期时间、添加慢查询日志这种需要高精度时间,还是会进行系统调用
14.2.2 更新LRU时钟
服务器状态的lruclock保存了服务器的LRU时钟,是服务器时间缓存的一种
struct redisServer{ //默认每10秒更新一次时钟缓存,用于计算键的空转时常 unsigned lruclock:22; }
每个redis对象都有一个lru属性,保存了对象最后一次被命令访问的时间
typedef struct redisObject{ unsigned lru:22; }robj;
服务器计算键的空转时间:lruclock属性记录的时间减去redis对象的lru时间,得到的是一个近似值
14.2.4 更新服务器内存峰值记录
每次serverCron执行时,会检查当前使用的内存大小,跟记录的峰值比较,取最大值,在需要的情况下更新到峰值
14.2.5 处理SIGTERM信号
启动服务器时,Redis会为SIGTERM信号关联处理器,当服务器接收到SIGTERM信号时,处理器会打开服务器的shutdown_asap标识
serverCron函数每次运行时,会检查shutdown_asap标识判断是否需要关闭服务器,在关闭服务器前会进行RDB持久化操作
14.2.6 管理客户端资源
serverCron函数每次运行时,会调用clientsCron函数,这个函数的功能是对一定数量的客户端检查:
- 是否已经和服务器的连接超时,如果超时,释放客户端
- 如果客户端在上一次执行命令请求后,输入缓冲区的大小超过了一定长度,Redis会释放客户端当前的输入缓冲区,并重新创建一个默认大小的输入缓冲区,防止客户端的输入缓冲区耗费过多内存
14.2.7 管理数据库资源
serverCron函数每次执行都会调用databasesCron函数,这个函数的功能是对数据库中的键进行检查,删除其中的过期键,并在需要时,对字典进行收缩操作。
14.2.8 执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE期间(写入RDB文件),收到的BGREWRITEAOF命令(重写AOF文件),会被延迟到BGSAVE执行完毕之后。服务器的aof_rewrite_sheduled标识记录了服务器是否延迟了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。
struct redisServer{ //记录执行BGSAVE命令的子进程的ID: //如果服务器没有在执行BGSAVE //那么这个属性的值为-1 pid_t rdb_child_pid; //记录执行BGREWRITEAOF命令的子进程的ID: //如果服务器没有在执行BGREWRITEAOF, //那么这个属性的值为-1 pid_t aof_child_pid; }
每次serverCron函数执行时,程序都会检查rdb_child_pid和aof_child_pid两个属性的值,值不为-1代表有正在执行的持久化操作
- 只要其中一个属性的值不为-1,程序就会执行wait3函数,检查子进程是否有信号发来服务器进程:
- 如果有信号到达,表示新的RDB文件已经生成完毕,或者AOF文件重写完毕,服务器需要将新生成的文件替换原有文件
- 如果信号没有到达,程序不进行操作
- 如果都为-1,程序将执行以下三个检查
- 检查是否有BGREWRITEAOF被延迟了,如果有的话,开始执行
- 检查服务器的自动保存条件是否已经满足,如果满足,并且服务器没有执行其他持久化操作,那么开始一次新的BGSAVE操作
- 检查服务器设置的AOF重写条件是否满足,如果满足,并且服务器没有执行其他持久化操作,那么开始一次新的BGREWRITEAOF操作
14.2.10 将AOF缓冲区中的内容写入AOF文件
如果AOF缓冲区有待写入的数据,serverCron函数会调用相关的程序,将缓冲区的内容写入到AOF文件
14.2.11 关闭异步客户端
在这一步,服务器会关闭输出缓冲区大小超出限制的客户端
14.3 初始化服务器
一个Redis服务器从启动到能接受客户端的命令请求,需要进行一系列的初始化过程
14.3.1 初始化服务器状态结构
创建一个struct redisServer类型的实例变量作为服务器的状态,并为结构中的各个属性设置默认值,初始化函数initServerConfig
- 设置服务器的运行ID
- 设置服务器的默认运行频率
- 设置服务器的默认配置文件
- 设置服务器的运行架构
- 设置服务器的默认端口号
- 设置服务器的默认RDB持久化条件和AOF持久化条件
- 初始化服务器的LRU时钟
- 创建命令表
14.3.2 载入配置选项
载入用户修改的配置参数和配置文件,对服务器状态中的属性进行修改
14.3.3 初始化服务器数据结构
调用initServer函数替服务器状态中的数据结构分配内存,包括客户端链表,数据库数组等等。除此之外,还进行一些非常重要的设置操作。
- 为服务器设置进程信号处理器
- 创建共享对象:"OK","ERR"等用于回复的字符串对象,整数1到10000的字符串对象
- 打开服务器监听端口,为监听套接字关联连接应答事件处理器,等待客户端的接入
- 为serverCron函数创建时间事件,等待服务器正式运行时运行serverCron函数
- 如果AOF持久化功能打开,那么打开现有的AOF文件;如果AOF文件不存在,打开并创建一个新的AOF文件,为AOF写入做准备
- 初始化服务器的后台I/O模块
initServerConfig函数主要负责初始化一般属性,initServer属性主要负责初始化数据结构。
当initServer函数执行完毕,打印Redis图标,及版本信息
14.3.4 还原数据库状态
完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,还原服务器的数据库状态。如果服务器启用了AOF持久化,会优先使用AOF文件还原,否则使用RDB文件,还原完毕将打印日志显示还原耗时
[5244]21 Nov 22:43:49.084 * DB loaded from disk: 0.068 seconds
14.3.5 执行事件循环
服务器打印日志
[5244] 21 Nov 22:43:49.084 * The server is now ready to accept connections on port 6379
开始执行服务器的事件循环