Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:

  • 文件事件:Redis服务端通过套接字与客户端(或其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
  • 时间事件:Redis服务器中的一些操作(如serverCron)函数需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

文件事件

      Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器:

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

      虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接。

文件事件处理器

      下图展示了文件事件处理器的四个组成部分:套接字I/O多路复用程序文件事件分派器(dispatcher)事件处理器
请输入图片描述

      文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。
      尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
请输入图片描述

      文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型调用相应的事件处理器。

I/O多路复用程序

      Redis的I/O多路复用程序的所有功能都是通过包装常见的selectepollevportkqueue这些多路复用函数库来实现的。因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的。Redis在I/O多路复用程序的实现源码中用#include宏定义了相应的规则,程序会在编译时自动选择系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现(ae.c文件):

/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

I/O多路复用程序

      I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,这两类事件和套接字操作之间的对应关系如下:

  • 当套接字变得可读(READABLE,客户端对套接字执行write操作,或者执行close操作)时,或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
  • 当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件

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

文件事件处理器

      Redis为文件事件处理器编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求:

  • 为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器
  • 为了接受客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器
  • 为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器
  • 当主服务器和从服务器进行复制操作时,主从服务器都需要关联特别为复制功能编写的复制处理器

连接应答处理器

      networking.c/acceptTcpHandler函数是Redis的连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/acccept函数的包装。
      当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE时间关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接子应答操作。
请输入图片描述

命令请求处理器

      networking.c/readQueryFromClient函数是Redis的命令请求处理器,这个处理器负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read函数的包装。
      当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作。
      在客户端连接服务器的整个过程中,服务器都会一直为客户端套接字AE_READABLE事件关联命令请求处理器。
请输入图片描述

命令回复处理器

      networking.c/sendReplyToClient函数是Redis的命令回复处理器,这个处理器负责从服务器执行命令后得到的命令回复通过套接字返回给客户端,具体实现为unistd.h/write函数的包装。
      当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作。
      当命令回复发送完毕之后,服务器就会解除命令回复处理器与客户端套接字的AE_WRITABLE事件之间的关联。
请输入图片描述

一次通信过程

请输入图片描述

时间事件

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

  • 定时事件:让一段程序在指定的时间之后执行一次。
  • 周期性事件:让一段程序每隔指定时间就执行一次。

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

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

实现

      服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
请输入图片描述

注意,保存时间事件的链表为无序链表,指的不是链表不按ID排序,而是说该链表不按when属性的大小排序。正因为链表没有按when属性进行排序,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已到达的时间事件都会被处理。

def processTimeEvents():
    # 遍历服务器中的所有时间事件
    for time_event in all_time_event():
        
        # 检查事件是否已经到达
        if time_event.when <= unix_tx_now():

            # 事件已到达
            # 执行事件处理器,并获取返回值
            retval = time_event.timeProc()
            
            # 如果这是一个定时事件
            if retval == AE_NOMORE:
                # 那么将该事件从服务器中删除
                delete_time_event_from_server(time_event)
            # 这是一个周期性事件
            else:
                # 按照事件处理器的返回值更新事件的when属性
                # 让这个事件在指定的时间之后再次到达
                update_when(time_event,retval)

时间事件实例:serverCron函数

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

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

      Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。从Reids2.8开始,用户可以通过修改hz选项来调整serverCron的每秒执行次数。

事件的调度与执行

      因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等。
请输入图片描述

      事件的调度和执行由ae.c/aeProcessEvents函数负责,伪代码如下:

def main():
    # 初始化服务器
    init_server()
    
    # 一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
        aeProcessEvents()

    # 服务器关闭,执行清理操作
    clean_server()

def aeProcessEvents():
    # 获取到达时间离当前时间最接近的时间事件
    time_event = aeSearchNearestTimer()
    
    # 计算最接近的时间事件距离到达还有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()

    # 如果事件已到达,那么remaind_ms的值可能为负数,将它设定为0
    if remaind_ms < 0:
        remaind_ms = 0
    
    # 根据remaind_ms的值,创建timeval结构
    timeval = create_timeval_with_ms(remaind_ms)

    # 阻塞并等待文件事件产生,最大阻塞事件由传入的timeval结构决定
    # 如果remaind_ms的值为0,那么aeApiPoll调用之后马上返回,不阻塞
    aeApiPoll(timeval)

    # 处理所有已产生的文件事件
    processFileEvents()

    # 处理所有已到达的时间事件
    processTimeEvents()

重点回顾

  • Redis服务器是一个事件驱动程序,服务器处理的事件分为时间事件和文件事件两类
  • 文件事件处理器是基于Reactor模式实现的网络通信程序
  • 文件事件是对套接字操作的抽象:每次套接字变为可应答、可写或者可读时,相应的文件事件就会产生
  • 文件事件分为AE_READABLE事件和AE_WRITABLE事件两类
  • 时间事件分为定时事件和周期性事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次
  • 服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件
  • 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程也不会进行抢占
  • 时间事件的实际处理时间通常会比设定的到达时间晚一些
Last modification:September 29th, 2020 at 08:00 pm
如果觉得我的文章对你有用,请随意赞赏