Redis中的事件

Redis是一个事件驱动程序,Reids中服务器需要处理两类事件:文件事件和时间事件。其实就可以说通过事件让服务器来处理业务。下边就谈一谈Redis服务器中的两种事件。

文件事件

文件事件的概念

简而言之,文件事件就是服务端通过Socket和客户端进行交互的产物。说到这块,回想以前在Java写一个小型的网络聊天室用的Socket(套接字),Socket是必须要有服务端,然后客户端连接到服务端而发生会话。Redis服务器也是根据Socket进行和其他的客户端进行连接。服务端就监听和处理Socket通信产生的文件事件来完成一系列的网络操作。

采用的并发处理技术

前言了解Reactor模式:是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至相关的工作线程进行处理。

Redis是基于Reactor模式的开发了自己的文件事务处理器,不仅仅是Redis使用了Reactor,netty也同样使用了Reactor模式,这足以说明Reactor模式是一个优秀的存在。

文件事件处理器
文件事件处理器的工作流程
  1. 正如在前言里说的Reactor模式,是基于IO多路复用(multiplexing)同时监听多个套接字,并根据不同套接字当前执行的不同任务来为套接字关联对应的事件处理器
  2. 被监听的套接字准备好执行应答,读取,写入,关闭等操作时,就会产生与它操作相对应的文件事件,这时候文件事件处理器就会调用套接字关联好的事件处理器来处理这些事件。

读完这一段话要明白,文件事件处理器(可以看作为总处理器)和事件处理器(处理和套接字绑定的事件)和文件事件(服务器对套接字的响应)的来源及其作用。

文件事件处理器的构成

从网上找一张图来具体分析

redis使用哪种reactor模型_文件事件

在上图中的文件事件处理器可以分为四个部分:

  • 套接字:主要是用于服务器和客户端的通信
  • IO多路复用程序:多路(多个TCP或channel)的IO操作复用一个或者少量的线程。
  • 文件事件分派器:根据命令不同进行分派
  • 事件处理器:处理事件
文件事件处理流程
  1. 多个套接字对一个服务器进行连接请求并准备好执行连接应答,写入,读取,关闭等操作时产生多个文件事件。(文件事件是对套接字操作的抽象,多个文件事件一起向服务端发出会牵扯到并发)
  2. 因为上一步而产生的并发原因,因此就需要IO多路复用程序来进行处理(负责将并发套接字转换为一个队列),它负责监听多个套接字,并向文件分派器传送那些产生了事件的套接字,这些套接字被放在一个队列中。
  3. 文件事件分配器从IO多路复用那接受来第一个套接字,并根据套接字的类型来进行不同的处理,处理完第一个就又从队列中拿出第二个以至多个。

值得注意的是,在文件分配器进行分配和调用相关的事件处理器,只能按照对列一个一个来处理。

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

redis使用哪种reactor模型_套接字_02

I/O多路复用的底层就是由select,epoll,evport和kqueue组成。epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题。

就以select为例说明问题

redis使用哪种reactor模型_文件事件_03

先是注册事件,然后经过I/O多路复用(Reactor)监听套接字来穿给文件事件分配器具体交给内核处理,然后内核根据命令来进行不同的响应。

//包含系统支持中最好的多路复用
# 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

来尝试分析分析源码:它在首先判断判断EVPORT,然后是EPOLL,然后是KQUEUE,最后是SELECT。这也是根据效率由高到低进行判断。

Redis为每一个多路复用函数库都实现类相同的API,因此多路复用程序的底层是可以互换的,在Redis中的源码中在编译时会自动选择系统中性能最高的I/O多路复用作为Redis的I/O多路复用的底层。

事件的类型

在前面也说到过事件的类型,多路复用程序监听多个套接字的读和写

  • 可读:客户端对套接字执行write操作或close时使得套接字变得可读时,或者有新的可应答(acceptble)套接字出现时,套接字所产生的AE_READABLE事件。

科普:什么是可应答状态?就是客户端对服务端的监听套接字发connect请求。

  • 可写: 客户端对监听套接字执行read操作时此套接字就边的可写,这时套接字产生AE_WRITABLE。

一个套接字要是既读又写的话,先读套接字,后写套接字。

有关的API
  1. ae.c/aeCreateFileEvent:将给定事件加到多路复用的监听范围内
  2. ae.c/aeDeleteFileEvent:将给定事件从监听范围移除
  3. ae.c/aeGetFileEvents:接受一个套接字描述,返回套接字正被监听的类型
  • 如果该套接字没事件被监听:返回AE_NONE
  • 读被监听:AE_READABLE
  • 写被监听:AE_WRITABLE
  • 读和写都被监听,返回AE_READABLE|AE_WRITABLE

等等

文件事件的处理器

Redis中写了多个处理器用来实现不同的网络通信需求。比如:

  • 为了对连接的服务器进行应答,服务器要对监听套接字关联应答处理器。
  • 在Redis服务器初始化时,将连接应答处理器和服务器监听套接字关联起来,当客户端调用连接时就会产生此事件,引发连接应答器并执行响应的应答操作。
  • 为了接受服务端命令请求,为客户端套接字关联命令请求处理器。
  • 请求处理器负责从套接字读入客户端发送的命令请求内容,客户端发生请求命令就会生成相应事件执行请求处理器。
  • 为了向客户端返回命令执行结果,为客户端套接字关联回复处理器。
  • 和上面两个原理大致相同。
  • 主服务器和从服务器进行复制时,主服务器都要关联复制处理器。

时间事件

时间事件的种类

Redis中的事件事件主要分为两种:

  • 定时事件:在指定时间后执行
  • 周期事件:每隔多长时间执行一次

根据返回值来判断它的种类,如果是NOMORE那就是一次性的,否则就是周期性的。

时间事件的属性

  • id:全局唯一ID,从小到大,新事件的ID比旧事件大
  • when:记录事件的到达时间
  • timeproc:时间事件处理器,当时间事件到达,处理事件

时间事件的实现

服务器将所有的时间时间放在一个无序链表中,每当时间事件执行器运行时,它都会遍历整个链表,找出已到达时间的时间事件,然后调用相应的事件处理器。(通过指针操作来取值)

在Redis只使用serverCorn一个时间事件,在benchmark下也只使用两个时间事件。

几个简单的API

  • aeDeleteFileEvent:接受ID,从服务器中查找并删除该ID的时间事件
  • processFileEvent:事件执行器,遍历并找出事件到了的事件进行处理
  • 等等。。。。

serverCorn函数

持续运行的Redis服务器需要定期的自身检查,这些工作就交给了它。

  • 更新服务器的统计信息
  • 清理过期键值对
  • 关闭和清理失效的客户端
  • 尝试进行AOF和RDB持久化操作
  • 如果存在于主服务器,定期对从服务器进行同步
  • 如果是集群模式,定期对集群机型同步和连接测试

用户可以通过修改redis中配置文件的hz来修改serverCorn的每秒执行次数。

事件的调度和执行

在牵扯到并行的一些东西基本都离不开调度,一个好的调度策略可以让cpu发挥出最大的潜力。

下面是一个事件的执行流程。

redis使用哪种reactor模型_套接字_04

Redis中事件的调度和执行规则

  1. 程序等待文件的最大阻塞事件是由距离到达时间最近的事件决定的,这样可以按照不同的事件进行不同的分配,避免了忙等待,也保证了阻塞时间不会太长(超过事件需要使用的时间)。
  2. 由于底层采用的是无序链表,因此它并不能预测时间何时到来,因此在事件未到来时,服务器处于等待,当由事件到来就进行处理。
  3. 文件事件和时间事件的处理都是同步,有序,原子的进行执行,不会中途中断事件,也不会出现资源抢占的情况,无论是那种事件都会尽可能的减少阻塞时间。这和操作系统的时间分片也有相似的地方,每个人也由分配最大的时间片,如果在规定时间片内没有处理完,就循环到队尾下一次执行,也会把一些耗时的持久化操作放在子线程或者子进程下执行,尽量减少主进程的阻塞。
  4. 因为不能进行资源抢占,因此很多事件都不能按时完成(大多都会晚一点)。