Redis服务器是一个事件驱动程序,主要有两种:
简单来说,文件事件就是套接字操作相关的事件;时间事件就是定时操作相关事件。
Redis基于Reactor模式开发的网络事件处理器,就是文件事件处理器(file event handler):
使用IO多路复用监听多个套接字,虽然以单线程的方式运行,但文件事件处理器实现了高性能的网络通信模型,又能很好的与Redis服务器中其他模块对接,保持了设计的简单性。
由套接字,I/O多路复用程序,文件事件分派器,事件处理器组成。
文件事件是对套接字操作的抽象,当套接字准备好执行应答,读取,写入,关闭等操作时就会产生一个文件事件。一个服务器通常会监听多个套接字,因此文件事件可能会并发产生。但IO多路复用程序会将所有事件放在一个队列里,以有序,同步,一次一个套接字向文件事件分派器传送的姿态来运行。只有当上一个套接字产生事件被事件处理器执行完了,才会继续传送下一个套接字。
分派器根据接收到的套接字调用执行相应的事件处理器。
Redis对select、epoll、evport和kqueue等多路复用的函数库进行包装,每个多路复用函数库在其中都对应一个单独文件:ae_select.c,ae_epoll.c,ae_kqueue.c等。每个多路复用函数都实现了相同的API,所以多路复用程序的底层实现是可以互换的。Redis在多路复用程序源码中用宏定义了相应规则,使得程序在编译时自动选择系统中性能最高的I/O多路复用函数库。
多路复用程序可监听的套接字事件可分为ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件
I/O多路复用程序允许服务器同时监听者两个事件,如果某个套接字同时产生了两种事件,文件事件分派其会优先处理AE_READABLE,再处理AE_WRITABLE。
根据客户端的需要,事件处理器分为连接应答处理器,命令请求处理器,命令回复处理器,复制处理器,最常用的是前三个处理器。
连接应答处理器
networking.c/acceptTcpHandler函数是Redis的连接应答处理器,用于连接服务器监听套接字的客户端进行应答。
Redis服务器初始化时,程序就将连接应答处理器和服务器监听套接字的AE_READABLE事件关联,当客户端调用sys/socket.h/connect函数时连接服务器监听套接字时,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作。
简单来说就是客户端连接服务端被监听的套接字时,服务端套接字产生并触发读事件,连接应答处理器就会执行。
命令请求处理器
networking.c/readQueryFromClient函数是Redis命令请求处理器,主要负责从套接字中读入客户端发送的命令请求内容。
当客户端成功连接到服务器后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联。这个关联会在客户端连接过程中一直存在。
当客户端向服务器发送命令请求时,套接字产生AE_READABLE事件,引发命令请求处理器执行,执行相应套接字的读入操作。
简单来说就是客户端发送命令请求时,客户端套接字产生并触发读事件,命令请求处理器就会执行。
命令回复处理器
networking.c/sendReplyToClient函数是Redis的命令回复处理器,负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。
当需要回复命令结果时,服务器会将客户端套接字的AE_WRITEBLE事件和命令回复处理器关联,当客户端准备好接收回复时就会产生AE_WRITABLE事件,引发命令回复处理器执行。执行结束,服务器会解除命令回复处理器与客户端的套接字AE_WRITABLE事件之间的关联。
简单来说就是服务器发送命令回复时,客户端套接字产生并触发写事件,命令回复处理器就会执行。
复制处理器
主从服务器之间进行复制时使用,这里先不介绍。
一次完整的基于文件事件的服务器与客户端交互,相关处理器的处理过程:
时间事件可分为定时事件和周期性事件:
一个时间事件主要由以下三个属性组成:
一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:
redis中只使用了周期时间事件,没有使用定时时间事件。
实现
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,遍历整个链表,找到已到达的时间事件,调用相应的事件处理器。
无序链表指的是不按照when时间大小排序,并不是指不按id大小排序。
serverCron函数
很多情况下,Redis需要定期进行资源检查,状态同步等操作,就需要定期操作,而定期操作都是由serverCron函数负责的,也是时间事件的应用实例。默认每隔100ms执行一次,具体工作包括:
下面简单从几个方面出发,介绍serverCron的本职工作:
当服务器同时存在时间事件和文件事件,事件的调度由ae.c/aeProcessEvents函数负责。对于每一次事件循环,主要过程是:
redis服务器运行流程
文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权(比如时间事件执行时间太长调用子进程,文件事件操作数据量太大主动break,余下的下次再执行),从而降低造成事件饥饿的可能性。
redis服务器是典型的一对多服务器程序,一个服务器可以与多个客户端建立连接,每个客户端都可以向服务器发送命令。服务器基于由IO多路复用技术实现的事件处理器,单线程的处理命令。
Redis服务器的状态结构clients属性是链表,记录了所有与服务器相连的客户端结构,对客户端执行批量操作或查找操作,都可以通过clients链表完成。
struct redisServer{
//一个链表,保存了所有客户端状态
list *clents
...
};
typedef struct redisClient{
//套接字描述符
int fd;
//标志
int flags;
//输入缓冲区
sds querybuf;
//单个命令拆分的数组
robj **argv;
//argv数组的长度
int argc;
//命令函数,指向具体的redisCommand结构
struct redisCommand *cmd;
//固定大小输出缓冲区,默认16K
char buf[REDIS_REPLY_CHUNK_BYTES];
//buf已使用字节数
int bufpos;
//大小可变输出缓冲区
list *reply
//创建客户端的时间
time_t ctime;
//与服务器互动的最后时间
time_t lastinteraction;
//软性限制时间
time_t obuf_soft_limit_reached_time;
...
}redis client;
套接字描述符fd
根据客户端类型不同:
伪客户端就是用于处理的命令请求来源于AOF或Lua脚本,不需要套接字连接,也就不需要套接字记录符。普通客户端就是所有来源于网络需要套接字连接的客户端。
标志flags
标志flags记录了客户端的角色。有主从标志,Lua伪客户端标志,执行MONITOR标志…标志可以以二进制来拼接:flags:||…
输入缓冲区querybuf
输入缓冲区存储客户端输入的指令,大小根据输入内容动态缩小扩大,最大不可超过1G,否则导致服务器关闭该客户端。
命令与参数(argv,argc)
客户端输入的命令会先存放到数组argv中,其数据结构是这样的:
当客户端输入命令后,服务器根据argv[0]的值再命令表中查找(命令不区分大小写)对应命令的函数并给cmd赋值,cmd就是对应的命令函数相关的操作信息。
输出缓冲区(buf,bufpos,reply)
输出缓冲区有两个,一个大小固定,一个大小可变。大小固定的存储长度小的回复,比如OK,错误返回等。大小可变缓冲区保存长度较大的回复,比如长列表,大集合。
大小可变缓冲区由reply链表实现,利用链表结构存储若干和字符串对象,使得长度不会受到限制。数据结构如下:
时间(ctime,lastinteraction,obuf_soft_limit_reached_time)
服务器使用两种模式来限制客户端输出缓冲区的大小:
首先是Redis服务器初始化操作,服务器从启动到能够处理客户端的命令请求需要执行以下步骤:
初始化服务器状态结构
主要是对redisServer结构体的初始化,包括设置服务器运行ID,运行频率,设置配置文件路径,设置持久化条件,命令表创建等。
载入配置选项
根据用户设定的配置,对redisServer相关变量的值进行修改,比如端口号,数据库数量,RDB的压缩是否开启等等。其他属性还是沿用默认值。
初始化服务器数据结构
服务器必须先载入用户配置,才能对其他数据结构进行准确初始化。其他数据结构包括客户端链表,db数组,订阅信息,Lua脚本执行环境,慢查询日志相关属性等等。
还原数据库状态
载入RDB或AOF文件的数据恢复过程。
执行事件循环
至此,服务器可接收客户端请求并发送信息。
以SET key value为例,命令的执行过程是:
下面将按照步骤拆解为发送,读取查找,执行预备操作,调用实现函数,执行后续工作,回复,打印操作讲解。
发送
客户端接收命令请求时,会将命令根据协议转为固定格式再发送给服务器。
读取
当套接字因客户端的写入变得可读时,服务器会先读取协议格式内容并保存到输入缓冲区。命令分析,提取参数及个数,存入argv和argc属性。最后调用命令执行器。
命令执行器-查找命令的实现
命令表是一个字典,键是命令名字,值是redisCommand结构。几个重要属性如下:
查找命令表的过程就是找到redisCommand,把指针指向它:
命令执行器-执行预备操作
在命令真正执行前需要有预备操作保证命令可以被正确,顺利地执行。这个环节相当于一层过滤,比如检查命令是否正确,参数是否正确,身份验证是否通过,内存是否够用等等。保证配置生效,准确执行。
命令执行器-调用命令的实现函数
执行过程就是调用之前找到并指向的执行函数。通过client->cmd->proc(client);调用。然后将回复保存在客户端状态的输出缓冲区中,关联该套接字的命令回复处理器。
命令执行器-执行后续工作
有一些善后工作还将继续,比如慢查询日志记录,执行时长记录,AOF持久化,主服务器将命令传给从服务器。当这些都处理完后,服务器就继续从文件时间处理器中取出并执行下一个命令请求。
将命令回复发送给客户端
当客户端套接字变为可写状态,服务器执行命令回复处理器,将输出缓冲区的回复发送给客户端。
客户端接收并打印命令回复
将回复转为人类可读的格式,打印给用户看。
注:内容是从语雀上的学习笔记迁移过来的,主要参考自《Redis的设计与实现》有些参考来源已经无法追溯,侵权私删。