Redis是一个事件驱动程序,事件分为文件事件和时间事件
- 文件事件:你可以简单的理解为服务器通过对套接字的操作而产生一些列的网络通信,这个操作就分为两类:写事件与读事件
- 时间事件:Redis一些操作需要定点的时间去执行,这类事件称为时间事件
文件事件处理的内部底层时间其实就是IO多路复用,Redis基于Reactor模式开发了自己的网络事件处理器
- 文件事件处理器使用IO多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务为套接字关联不同的事件处理器
- 当被监听到的套接字准备好了应答、读、写等操作,与之对应的文件事件就会产生,这时文件处理器就会调用套接字之间关联好的时间处理器来处理事件
如下图所示,总计有四部分组成:套接字、多路复用器、文件事件分派器和事件处理器

多路复用的工作流程
尽管多个套接字可能会并发的出现,但多路复用器会将所有产生事件的套接字放在一个队列中,然后通过这个队列以有序、同步,每次以一个套接字方式向文件分派器传送套接字,当上个套接字与之关联的时间处理器执行完毕,多路复用才会继续向这个类型的事件处理器派送下一个套接字
这些事件处理器实际上都是一个一个的函数,当某些事件发送时,服务器应该执行某些动作
多路复用器的功能就是通过包装简单的select、epoll、evport这些简单的库函数来实现的,每个对应到Redis中都是一个单独的文件
事件的类型你就记着,在底层要不就是读要不就是写,对应到Redis中
- 当套接字变成可读时(反向的就是客户端在往缓冲区写东西啊),或者有新的应答连接时,套接字产生
AE_READABLE- 当套接字变成可写时(反向同理就是客户端在缓冲区读东西呢),就会产出
AE_WRITABLE
多路复用允许上面两个时间同时发生,但在处理的时候文件派发器会优先处理读事件,再去处理写事件,即读套接字
-》写套接字的处理流程
- 如果套接字没有被监听,返回NONE
- 被读监听,返回READABLE
- 被写监听,返回WRITABLE
- 被同时监听,返回两个的或关系
对于实现不同的网络通信需求,Redis实现了四种事件处理器:
- 为了处理连接,服务器给多个客户端套接字关联处理连接的处理器
- 接收客户端传来的命令请求,关联命令请求处理器
- 为了向客户端返回命令的执行结果,命令回复处理器
- 主从复制,复制功能处理器
当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的
AE READABIE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行并执行相应的套接字应答操作,这个就是典型的Reactor模式,当某个请求操作来时,回调函数就是开始执行
命令请求处理器

命令回复处理器

整个流程

- 定时事件:让一段程序在指定时间内执行一次,返回ae.h/NOMORE,返回后删除
- 周期性事件:让程序每隔一段时间就执行一次,返回非NOMORE的整数值,每次返回对when进行更新
- id:时间事件的全局唯一ID,从大到小递增
- when:毫秒进度的unix时间戳,记录了时间事件的到达时间
- timeProc:时间事件处理器,一个函数,当时间到达时,服务调用相应的处理器来处理事件
服务器将所有的时间事件全部放在一个无序链表中,每当时间事件执行器运行时,就遍历整个链表,查询已经到达时间的,并调整响应的事件处理器
说的无序并不是指插入不按顺序,而是不按照when属性的顺序,所以你不知道谁到期没到期,即要遍历整个链表
一般情况下整个redis服务器都只有这一个时间事件,来确保资源和状态的稳定,从而使服务可以长期稳定的运行,这个函数的工作主要包括,默认每秒运行10次,可以自定义
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
- 清理数据库中的过期键值对。
- 关闭和清理连接失效的客户端。
- 尝试进行AOF 或RDB持久化操作。
- 如果服务器是主服务器,那么对从服务器进行定期同步。
- 如果处于集群模式,对集群进行定期同步和连接测试。
由于服务器用时存在两种的时间,那么怎么调度执行,何时处理文件事件又何时处理时间事件等等asProcessEvents函数中就对于这些事件的处理顺序做了定义,放在一个循环中即为Redis的主函数
Redis的使用场景中都是一对多的服务器程序,通过服务器的结构中的clients属性,很轻易的就能管理很多个客户端
一般分为通用与特定的客户端属性
客户端状态的fd属性记录了客户端正在使用的套接字描述符,根据客户端的类型不同,fd的值可以是-1或大于-1的整数
typedef struct redisClient{
int fd;
}redisClient;
(fd = -1)伪客户端处理的命令请求来源于AOF文件和Lua脚本,这也是伪客户端的两个用途,还原数据库状态和执行Lua脚本中Redis的命令。由于命令不是来源于网络,所以不需要套接字连接,自然也就不需要套接字描述符
(fd > -1)通过fd属性来关联套接字
#使用这个命令,下面属性的值的变化
| 属性名 | 分类 | 作用 |
|---|---|---|
| 名字 | \ | 给客户端命名使之更加清晰,如果不命名为NULL |
| 标志(flags) | 单个标志:flags=<flag>多个标志:flags=<flag1>.. | 每个标志位使用一个常量表示,一部分记录客户端的角色,另外则记录一些状态 |
| 输入缓冲区(querybuf) | 该属性是一个的值就是你的命令值![]() | 客户端状态的输入缓冲区用于保存客户端发送的命令请求 |
| 命令与命令参数(argv和argc) | argv指向具体命令的数组,argc是数组的长度![]() | 服务器对客户端输入缓冲区进行分析后,将得出的这两个参数值保存回来 |
| 命令的实现函数(redisCommend) | cmd指向这个结构![]() | 根据上面两个参数服务器选出对应命令执行 |
| 固定输出缓冲区(buf数组和bufpos表示使用的容量) | 固定长度![]() | 执行命令所得回复会保存在客户端的输出缓冲区 |
| 可变输出缓冲区(reply链表) | 可变![]() | 同理上面的作用 |
| 身份验证(authenicated) | 属性值为0(未通过)或1(通过) | 用于记录客户端是否通过身份验证 |
| 时间 | 很多 |
调用connect函数连接服务器
总之就是分为主动关闭和被动关闭,被动无非就是不符合规范了,命令回复大于客户端输出缓冲区或者命令请求大于了输入缓冲区
这里简单提一嘴,为了限制回复命令太长,我们直接限制输出缓冲区的大小,设置两个参数一个硬性限制和软性限制,达到硬性直接关闭连接,如果超过软性限制但未超过硬性设置,这时会有一个时间倒计时,如果时间内还在这个范围那么直接关闭,时间内小于软性了就继续
使用伪客户端,都是在不影响主线程处理命令的情况下,不需要网络连接的业务要求,lua的伪客户端会一直在服务器被关闭才会关闭,而AOF的伪客户端完成任务即关闭
对于这个命令,客户端和服务端共需要下面的操作
redis> set key value
OK
SET KEY VALUE。SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK。OK发送给客户端。OK,并将这个回复打印给用户观看。用户在客户端输入命令,客户端会将这个命令请求转换成协议格式,然后通过套接字,将协议格式的命令请求发送出去
因为客户端发送命令,这个套接字变得可读,服务器会读取套接字的命令请求,并保存在客户端状态的输入缓冲区里面,然后对缓冲区中的命令进行分析,提取出命令请求的命令参数以及个数,然后保存到客户端状态中的argv和argc属性中,调用命令执行器,执行客户端指定的命令
总的来说就是根据客户端状态的argv[0]参数,在命令表中找指定命令,找到的命令保存在客户端状态的cmd属性中。命令是一个字典,字典的键是一个个命令名字,字典的值是一个redisCommand结构,结构中就是命令后面附加的参数个数啊,标示值,命令的执行次数啊等等
命令对大小写不敏感,你写
Set sEt都可
在真正执行命令之前,需要一些预备操作,例如检查cmd的指针是否指向null、命令结构的参数个数属性对应命令带的参数数量是否对应、是否客户端服务端身份验证已经通过、内存是否够用,RDB备份失败,如果现在是一个写命令会返回错误其他的如下
上面两步全部完成,那么执行下面的伪代码就好,执行成功后,然后服务器会把OK\r\n写入到客户端状态的buf中
比如什么慢查询日志功能,功能表结构的更新、AOF的命令写入到缓冲区、主从服务器的命令传播等等
前面说过,命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备
当客户端接收到协议格式的命令回复,将这些回复进行反序列化变为可读然后打印给用户看
在时间事件中我们稍微介绍过serverCron的函数,1秒会执行10次来检查服务器的资源,并保持服务器良好运转
- 更新服务器时间缓存
- 更新LRU时钟
- 更新服务器每秒执行命令次数
- 更新服务器内存峰值记录
- 处理SIGTERM信号:SIGTERM信号是用于终止程序的通用信号。 SIGTERM提供了一种优雅的方式来杀死程序。是采取标志位标记的形式,而不是立马响应终止
- 管理客户端资源
- 管理数据库资源
- 执行被延迟的BGREWRITEAOF
- 检查持久化操作的运行状态
- 将AOF缓冲区中内容写入AOF文件
- 关闭异步客户端:客户端输出缓冲区大小超出限制
- 增加cronloops计数器的数值:这个参数记录了serverCron函数执行的次数
初始化服务器总计可分为五步:初始化服务器状态结构,载入配置选项、初始化服务器数据结构、还原数据库状态和执行事件循环
创建一个redisServer类型的实例变量作为服务器的状态,并为结构的各个属性设置默认值,这个赋值初始化是由一个initServerConfig的函数负责的,此函数的主要功能如下
用户可以通过给定配置参数或指定配置文件来修改服务器的配置,我们可以在命令行的状态下修改,这里你就记得配置大于约定
这里就是初始化除了命令表,redisServer中的其他数据结构例如什么
clients、db数组啊之类,之后还会调用initServer函数,为这些数据结构分配内存,并在需要时会为这些数据结构设置或关联初始值,这个函数的其他功能
在完成上述的初始化,服务器会根据是否开启持久化来还原数据库状态。如果AOF开启了,直接用AOF,没有AOF就用RDB文件
现在就开始服务器的事件循环了,可以接收命令请求并处理