Redis的设计与实现(4):什么是Redis的事件驱动程序

六、事件

Redis服务器是一个事件驱动程序,主要有两种:

  • 文件事件:Redis服务器通过套接字与客户端连接,文件事件就是服务器对套接字操作的抽象。服务器与客户端通信会产生相应文件事件,服务器通过监听这些事件来完成一系列网络通信操作。
  • 时间事件:Redis服务器有一些需要在给定时间内执行的操作,而时间事件就是对这类定时操作的抽象。

简单来说,文件事件就是套接字操作相关的事件;时间事件就是定时操作相关事件

6.1文件事件

Redis基于Reactor模式开发的网络事件处理器,就是文件事件处理器(file event handler):

  • 使用I/O多路复用程序同时监听多个套接字,根据套接字目前执行的任务为套接字关联不同的事件处理器
  • 当被监听的套接字准备好应答,读取,写入,关闭等操作时,与之对应的文件事件就会产生,文件事件处理器就会调用套接字之前关联好的事件处理器来处理事件。

使用IO多路复用监听多个套接字,虽然以单线程的方式运行,但文件事件处理器实现了高性能的网络通信模型,又能很好的与Redis服务器中其他模块对接,保持了设计的简单性。

文件事件的构成
Redis的设计与实现(4):什么是Redis的事件驱动程序_第1张图片

由套接字,I/O多路复用程序,文件事件分派器,事件处理器组成。
文件事件是对套接字操作的抽象,当套接字准备好执行应答,读取,写入,关闭等操作时就会产生一个文件事件。一个服务器通常会监听多个套接字,因此文件事件可能会并发产生。但IO多路复用程序会将所有事件放在一个队列里,以有序,同步,一次一个套接字向文件事件分派器传送的姿态来运行。只有当上一个套接字产生事件被事件处理器执行完了,才会继续传送下一个套接字。
image.png
分派器根据接收到的套接字调用执行相应的事件处理器。

6.1.1多路复用的实现

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事件

  • 当套接字变得可读时(客户端对套接字执行write,close,accept后),套接字产生AE_READABLE事件。
  • 当套接字变得可写时(客户端对套接字执行read操作后),套接字产生AE_WRITABLE事件。

I/O多路复用程序允许服务器同时监听者两个事件,如果某个套接字同时产生了两种事件,文件事件分派其会优先处理AE_READABLE,再处理AE_WRITABLE。

6.1.2文件事件处理器

根据客户端的需要,事件处理器分为连接应答处理器,命令请求处理器,命令回复处理器,复制处理器,最常用的是前三个处理器。

  • 连接应答处理器
    networking.c/acceptTcpHandler函数是Redis的连接应答处理器,用于连接服务器监听套接字的客户端进行应答。
    Redis服务器初始化时,程序就将连接应答处理器和服务器监听套接字的AE_READABLE事件关联,当客户端调用sys/socket.h/connect函数时连接服务器监听套接字时,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作。
    简单来说就是客户端连接服务端被监听的套接字时,服务端套接字产生并触发读事件,连接应答处理器就会执行。
    Redis的设计与实现(4):什么是Redis的事件驱动程序_第2张图片

  • 命令请求处理器
    networking.c/readQueryFromClient函数是Redis命令请求处理器,主要负责从套接字中读入客户端发送的命令请求内容。
    当客户端成功连接到服务器后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联。这个关联会在客户端连接过程中一直存在
    当客户端向服务器发送命令请求时,套接字产生AE_READABLE事件,引发命令请求处理器执行,执行相应套接字的读入操作。
    简单来说就是客户端发送命令请求时,客户端套接字产生并触发读事件,命令请求处理器就会执行。
    Redis的设计与实现(4):什么是Redis的事件驱动程序_第3张图片

  • 命令回复处理器
    networking.c/sendReplyToClient函数是Redis的命令回复处理器,负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。
    当需要回复命令结果时,服务器会将客户端套接字的AE_WRITEBLE事件和命令回复处理器关联,当客户端准备好接收回复时就会产生AE_WRITABLE事件,引发命令回复处理器执行。执行结束,服务器会解除命令回复处理器与客户端的套接字AE_WRITABLE事件之间的关联。
    简单来说就是服务器发送命令回复时,客户端套接字产生并触发写事件,命令回复处理器就会执行。

  • 复制处理器
    主从服务器之间进行复制时使用,这里先不介绍。

一次完整的基于文件事件的服务器与客户端交互,相关处理器的处理过程:

  1. 客户端发起连接,服务器监听套接字产生AE_READABLE事件,触发连接应答处理器执行。然后创建客户端套接字,客户端状态并将客户端套接字的AE_READABLE事件与命令请求处理器关联,然后客户端可以向服务器发送命令。
  2. 客户端发送命令,客户端套接字产生AE_READABLE事件,触发命令请求处理器,执行器读取执行命令,由相关程序执行具体操作。产生相应的命令回复后,服务器将客户端套接字的AE_WRITEBLE事件与命令回复处理器关联。
  3. 客户端尝试读取命令回复,产生AE_WRITEBLE事件,触发命令回复处理器执行。将回复写入套接字,解除读事件与命令回复处理器的关联。

Redis的设计与实现(4):什么是Redis的事件驱动程序_第4张图片

6.2时间事件

时间事件可分为定时事件周期性事件

  • 定时事件:只在指定事件到达执行一次。如xx时间后执行一次。
  • 周期性事件:每隔一段时间执行一次。如每隔xx秒执行一次。

一个时间事件主要由以下三个属性组成:

  • id:服务器为时间事件创建的全局唯一ID (标识号)。 ID号按从小到大的顺序递增,新事件的ID号比旧事件大。
  • when:毫秒精度的UNIX时间戳,时间事件的到达(arrive)时间。
  • timeProc:时间事件处理器函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。

一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:

  • 事件处理器返回ae.h/AE_NOMORE, 为定时事件:该事件在达到一次之后被删除,之后不再到达。
  • 事件处理器返回非AE_NOMORE的整数值,为周期性时间。当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。

redis中只使用了周期时间事件,没有使用定时时间事件。

实现

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,遍历整个链表,找到已到达的时间事件,调用相应的事件处理器。
无序链表指的是不按照when时间大小排序,并不是指不按id大小排序。
Redis的设计与实现(4):什么是Redis的事件驱动程序_第5张图片
serverCron函数
很多情况下,Redis需要定期进行资源检查,状态同步等操作,就需要定期操作,而定期操作都是由serverCron函数负责的,也是时间事件的应用实例。默认每隔100ms执行一次,具体工作包括:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
  • 清理数据库中的过期键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行AOF或RDB持久化操作。
  • 如果服务器是主服务器,那么对从服务器进行定期同步。
  • 如果处于集群模式,对集群进行定期同步和连接测试。

下面简单从几个方面出发,介绍serverCron的本职工作:

  • 更新服务器时间缓存,默认100毫秒更新一次unixtime和mstime
  • 更新LRU时钟,每个Redis对象也会有lru属性,记录上一次被命令访问的时间。如果要计算一个键的空转时长,就要通过lrulock记录的时间减去对象的lru属性记录时间。
  • 更新服务器每秒执行命令数,这个估计值会被放入redisServer的ops_sec_samples数组中。
  • 更新服务器内存峰值记录,stat_peak_memory记录内存峰值。
  • 管理客户端资源,调用clientsCron函数对客户端进行检查:如果已经超时则关闭;如果输入缓冲区大小超过一定长度则重新创建默认大小的输入缓冲区。
  • 管理数据库资源,serverCron每次执行都会调用databaseCron函数,会对服务器的一部分数据库检查,删除过期键;对字典收缩。
  • 执行被延迟的BGREWRITEAOF,aof_rewrite_shceduled标志位决定,BGSAVE命令执行期间,BGREWRITEAOF会被延迟到BGSAVE执行后执行。
  • 检查持久化操作的运行状态

6.3事件的调度

当服务器同时存在时间事件和文件事件,事件的调度由ae.c/aeProcessEvents函数负责。对于每一次事件循环,主要过程是:

  1. 拿到最近的时间事件并计算还有多少毫秒。
  2. 创建时间任务结构;阻塞等待文件事件产生,最大阻塞时间由最近时间事件到达毫秒数决定
  3. 先处理已产生的文件事件再处理到达的时间事件

redis服务器运行流程
Redis的设计与实现(4):什么是Redis的事件驱动程序_第6张图片
文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权(比如时间事件执行时间太长调用子进程,文件事件操作数据量太大主动break,余下的下次再执行),从而降低造成事件饥饿的可能性。

七、客户端

redis服务器是典型的一对多服务器程序,一个服务器可以与多个客户端建立连接,每个客户端都可以向服务器发送命令。服务器基于由IO多路复用技术实现的事件处理器,单线程的处理命令。
Redis服务器的状态结构clients属性是链表,记录了所有与服务器相连的客户端结构,对客户端执行批量操作或查找操作,都可以通过clients链表完成。

struct redisServer{
    //一个链表,保存了所有客户端状态
    list *clents
    ...
};

Redis的设计与实现(4):什么是Redis的事件驱动程序_第7张图片
每个客户端的结构为

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
根据客户端类型不同:

  • fd为-1表示伪客户端。
  • fd为大于-1的整数时表示普通客户端。

伪客户端就是用于处理的命令请求来源于AOF或Lua脚本,不需要套接字连接,也就不需要套接字记录符。普通客户端就是所有来源于网络需要套接字连接的客户端。
标志flags
标志flags记录了客户端的角色。有主从标志,Lua伪客户端标志,执行MONITOR标志…标志可以以二进制来拼接:flags:||…
输入缓冲区querybuf
输入缓冲区存储客户端输入的指令,大小根据输入内容动态缩小扩大,最大不可超过1G,否则导致服务器关闭该客户端。
命令与参数(argv,argc)
客户端输入的命令会先存放到数组argv中,其数据结构是这样的:
Redis的设计与实现(4):什么是Redis的事件驱动程序_第8张图片
当客户端输入命令后,服务器根据argv[0]的值再命令表中查找(命令不区分大小写)对应命令的函数并给cmd赋值,cmd就是对应的命令函数相关的操作信息。
输出缓冲区(buf,bufpos,reply)
输出缓冲区有两个,一个大小固定,一个大小可变。大小固定的存储长度小的回复,比如OK,错误返回等。大小可变缓冲区保存长度较大的回复,比如长列表,大集合。
大小可变缓冲区由reply链表实现,利用链表结构存储若干和字符串对象,使得长度不会受到限制。数据结构如下:
时间(ctime,lastinteraction,obuf_soft_limit_reached_time)
服务器使用两种模式来限制客户端输出缓冲区的大小:

  • 硬性限制( hard limit):如果输出缓冲区的大小超过了硬性限制所设置的大小,那么服务器立即关闭客户端。
  • 软性限制(softlimit):软性限制比硬性限制小,服务器会根据输出缓冲区大小介于软硬性限制之间的时间决定是否关闭客户端 。

八、服务端

8.1初始化服务器

首先是Redis服务器初始化操作,服务器从启动到能够处理客户端的命令请求需要执行以下步骤:

  1. 初始化服务器状态。
  2. 载入服务器配置。
  3. 初始化服务器数据结构。
  4. 还原数据库状态。
  5. 执行事件循环。

初始化服务器状态结构
主要是对redisServer结构体的初始化,包括设置服务器运行ID,运行频率,设置配置文件路径,设置持久化条件,命令表创建等。
载入配置选项
根据用户设定的配置,对redisServer相关变量的值进行修改,比如端口号,数据库数量,RDB的压缩是否开启等等。其他属性还是沿用默认值。
初始化服务器数据结构
服务器必须先载入用户配置,才能对其他数据结构进行准确初始化。其他数据结构包括客户端链表,db数组,订阅信息,Lua脚本执行环境,慢查询日志相关属性等等。
还原数据库状态
载入RDB或AOF文件的数据恢复过程。
执行事件循环
至此,服务器可接收客户端请求并发送信息。

8.2命令执行过程

以SET key value为例,命令的执行过程是:

  1. 客户端发送命令。
  2. 服务器接收并处理请求,对数据库操作,回复OK。
  3. 服务器将命令回复给客户端。
  4. 客户端接收命令并打印结果。

下面将按照步骤拆解为发送,读取查找,执行预备操作,调用实现函数,执行后续工作,回复,打印操作讲解。
发送
客户端接收命令请求时,会将命令根据协议转为固定格式再发送给服务器。
读取
当套接字因客户端的写入变得可读时,服务器会先读取协议格式内容并保存到输入缓冲区。命令分析,提取参数及个数,存入argv和argc属性。最后调用命令执行器。
命令执行器-查找命令的实现
命令表是一个字典,键是命令名字,值是redisCommand结构。几个重要属性如下:

  • name:命令名称。
  • proc:指向命令实现函数。
  • arity:命令参数个数,包括命令名称。
  • sflags:命令属性。

查找命令表的过程就是找到redisCommand,把指针指向它:
Redis的设计与实现(4):什么是Redis的事件驱动程序_第9张图片
命令执行器-执行预备操作
在命令真正执行前需要有预备操作保证命令可以被正确,顺利地执行。这个环节相当于一层过滤,比如检查命令是否正确,参数是否正确,身份验证是否通过,内存是否够用等等。保证配置生效,准确执行。
命令执行器-调用命令的实现函数
执行过程就是调用之前找到并指向的执行函数。通过client->cmd->proc(client);调用。然后将回复保存在客户端状态的输出缓冲区中,关联该套接字的命令回复处理器。
命令执行器-执行后续工作
有一些善后工作还将继续,比如慢查询日志记录,执行时长记录,AOF持久化,主服务器将命令传给从服务器。当这些都处理完后,服务器就继续从文件时间处理器中取出并执行下一个命令请求。
将命令回复发送给客户端
当客户端套接字变为可写状态,服务器执行命令回复处理器,将输出缓冲区的回复发送给客户端。
客户端接收并打印命令回复
将回复转为人类可读的格式,打印给用户看。

注:内容是从语雀上的学习笔记迁移过来的,主要参考自《Redis的设计与实现》有些参考来源已经无法追溯,侵权私删。

你可能感兴趣的:(Redis,redis)