深入了解Redis之事件原理和实现

Redis服务器是一个事件驱动程序,其主要处理的事件主要可分为以下两类:

  • 文件事件(file event)

    Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信。

  • 时间事件(time event)

    Redis服务器中的一些操作(如,serverCron函数)需要在指定的时间点执行,而时间事件就是对这类定时操作的抽象。

文件事件

文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。

Redis基于Reactor模式开发了自己的文件事件(网络事件)处理器(file event handler)。其构成可分为四个部分,套接字、I/O多路复用程序、文件事件分派器(dispatcher),以及事件处理器。

事件处理器的四个组成部分

  • 因为一个服务器通常会连接多个套接字,所以多个文件事件可能会并发的出现
  • I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。(尽管会有多个文件事件同时出现,但是I/O多路复用程序会将之转化成一个有序的队列,依次将其传送给文件事件分派器。并且等上一个套接字处理完毕之后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字)
  • 文件事件分派器接受I/O多路复用程序传送过来的套接字,并根据套接字产生的事件类型,调用相应的事件处理器。

    服务器会为执行不同任务的套接字关联不同的事件处理器,如有应答处理器、命令请求处理器、命令回复处理器。

    I/O多路复用程序的实现方式

Redis的I/O复用程序的功能都是通过包装了常见的I/O多路复用函数库来实现的

  • select
  • epoll
  • evport
  • kqueue

I/O多路复用函数库的选择

Redis用每个函数库都实现了相同的API,程序会在编译时自动判断当前系统下哪种多路复用函数库性能最高,而选择其作为Redis的I/O多路复用程序的底层实现。

文件事件的类型

  • AE_READABLE 事件

    当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作)

  • AE_WRITABLE 事件

    当套接字变得可写时(客户端对套接字执行read操作)

I/O多路复用程序允许服务器同时监听套接字的 AE_READABLE 和 AE_WRITABLE 事件,并且如果一个套接字同时出现这两种事件,那么文件分派器会优先处理 AE_READABLE 事件,然后才会处理 AE_WRITABLE 事件。

文件事件处理器

  • 连接应答处理器

服务器对客户端的连接请求进行应答

当Redis服务器进行初始化的时候,会将连接应答处理器和服务器监听套接字的 AE_READABLE 事件关联起来,当有客户端与服务器监听的套接字进行连接时,套接字就会产生 AE_READABLE 事件,引发连接应答处理器执行,并执行相应的套接字应答操作。

  • 命令请求处理器

服务器接收客户端发来的命令请求
当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的 AE_READABLE 事件和命令请求处理器关联起来,当客户端向服务器端发送请求命令的时候,套接字就会产生 AE_READABLE 事件,引发命令请求处理器执行,并执行相应的套接字读入操作。

在客户端与服务器连接的整个过程中,服务器都会一直为客户端套接字的 AE_READABLE 事件关联命令请求处理器。

  • 命令回复处理器

服务器向客户端发送命令回复

当客户端有命令回复需要传送给客户端的时候,服务器会将客户端套接字的 AE_WRITABLE 事件和命令回复处理器关联起来,当客户端准备好接受服务器传回的命令回复时,就会产生 AE_WRITABLE 事件,引发命令回复处理器执行,并执行相应的套接字写入操作。

一次完整的客户端与服务器连接事件的示例

客户端和服务器的通信过程

假设一个Redis服务器正在运行,那么这个服务器的监听套接字的 AE_READABLE 事件应该正在处于监听状态下,而该事件所对应的处理器为连接应答处理器。

这是如果有一个Redis客户端向服务器发起连接,那么监听套接字将会产生 AE_READABLE 事件,I/O多路复用程序将会将这个 AE_READABLE 事件传送给文件事件分派器,文件分派器将其分给连接应答处理器触发其执行。

应答处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端的套接字的 AE_READABLE 事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。

之后,假设客户端向主服务器发送命令请求,那么客户端套接字将产生 AE_READABLE 事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。

执行程序执行完毕将会有对应的回复,为了将这些命令回复传送给客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候,客户端套接字将会产生 AE_WRITABLE 事件,触发命令回复处理器执行,当命令回复处理器将命令回复全部写入到套接字之后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。

时间事件

Redis的时间事件分为以下两类:

  • 定时事件
  • 周期性事件

目前版本的Redis只使用周期性事件,而没有使用定时事件。

时间事件的组成

一个时间事件由以下三个属性组成,

  • id,服务器为时间事件生成的全局唯一的ID(标志号)。该ID是自增的,即新的事件ID总是比旧事件ID要大。

  • when,毫秒精度的时间戳,记录了事件的到达(arrive)时间

  • timeProc,时间事件处理器,一个函数

不管是定期时间事件和还是周期性时间事件,其组成都是由这三个属性构成。其不同取决于时间事件处理器的返回值

  • 如果事件处理器返回 AE_NOMARE ,那么这个事件就是定时事件。该事件在第一次之后就会被删除,之后不在到达。
  • 如果事件处理器返回 非AE_NOMARE 的整数值,那么这个事件就是周期性事件,该事件在到达之后,服务器会根据这个返回的值,对时间事件的 when 属性进行更新,让这个事件在一段时间之后可以再次到达。

实现

Redis 服务器将所有的时间事件都放在了一个无序列表中,每当时间事件执行器运行时,它就会遍历成哥链表,查找所有已到达的时间事件,并调用相应的事件处理器。

时间事件实现

实例

持续运行的Redis服务器需要定期的对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定的运行,这些定期操作是由 serverCron 函数负责执行。它的主要工作有,

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

Redis以周期性时间事件方式来运行 serverCron 函数,直至服务器关闭为止。

文件事件和时间事件的关系

事件角度下的服务器运行流程

文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件。并且由于文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器也不会终端正在执行的事件处理,也不会对事件进行抢占。

Redis事件调度和执行的几条规则

  • 由于文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍然未有任何事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时,服务器就可以开始处理到达的时间事件了。

  • 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间一般会比设定的时间稍晚一些。