11 事件
Redis服务器是一个事件驱动程序,服务器会处理以下两类事件
- 文件事件:Redis服务器通过套接字与客服端进行连接通讯,而服务器本身对套接字进行处理,以套接字为中转实现服务器与客服端的交流,而文件事件就是服务器对套接字操作的抽象。服务器与客服端的通信就会产生相应的文件事件,而服务器就是监听并处理这些事件来完成一系列网络通信的。
- 时间事件:Redis服务器中一些操作(serverCron函数)需要再给定时间点执行,而时间事件就是服务器对这类定时操作的抽象。
11.1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器,这个处理器称为文件事件处理器
- 文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的文件处理器。
- 当被监听的套接字准备好执行连接应答,读取,写入,关闭等操作时,就会产生相应的文件事件,这时文件事件处理器就会处理对应的文件事件
注:虽然文件事件处理器以单线程运行,但使用I/O多路复用程序来同时监听多个套接字,文件事件处理器既实现高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性
11.1.1 文件事件处理器的构成
文件事件处理器有四个部分,分别为套接字(连接客服端使用,有多个),I/O多路复用程序,文件事件分派器,事件处理器(多种)
-
文件事件是服务器对套接字进行操作的抽象,每当套接字准备执行连接应答、写入、读取、关闭就会产生一个文件事件,由于套接字有多个,所以多个文件事件可能并发的出现
-
I/O多路复用程序来同时监听多个套接字,并把产生事件的套接字传送给文件事件分派器
-
多个文件事件可能并发的出现,所以I/O多路复用程序将所有产生文件事件的套接字放进一个队列中,通过这个队列,以有序,同步,每次传送一个套接字的方式将套接字传送给文件事件分派器,每当事件处理器处理完一个事件才会传送下一个套接字。
-
文件事件分配器会根据套接字为套接字产生的事件的类型调用相应的事件处理器
-
事件处理器是一个个函数,它们定义在某个事件发生时,服务器应执行的动作
11.1.2 I/O多路复用程序的实现
Redis的I/O多路复用程序的所有功能都是通过包装常见的I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件。因为Redis为每个I/O多路复用函数库都实现了相同的API,所有I/O多路复用程序的底层实现是可以相互交换的,编译时会自动选择系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用函数库的底层实现。
11.1.3 事件的类型
I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件(读写的相对关系是服务器对套接字),两类事件和套接字的关系如下
-
当套接字变为可读时(客服端发送命令,进行write操作,这时服务器对客服端是读操作;或执行close操作),或者有新的可应答套接字出现,此时套接字产生AE_READABLE事件
-
当套接字变为可写时(服务器回复客服端时需要将内容写到套接字中),套接字产生AE_WRITABLE事件
I/O多路服务程序运行服务器同时监听这两种事件,同时产生时文件事件分派器优先处理AE_READABLE事件,等AE_READABLE事件处理完,再处理 AE_WRITABLE事件。
11.1.4 文件事件的处理器
1 连接应答处理器
这个处理器用于对连接服务器监听套接字的客服端进行应答,Redis服务器初始化时,程序会将这个连接应答处理器和服务器监听套接字的AE_REDABLE事件关联起来,引发应答处理器执行
2 命令请求处理器
这个处理器负责从套接字中读入客服端发送的命令等内容,当客服端用过连接应答处理器成功连接到服务器后,程序会将这个连接应答处理器和服务器监听套接字的AE_REDABLE事件关联起来,当客服端向服务器发送命令请求的时候,套接字就产生AE_REDABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作。
3 命令回复处理器
这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客服端。当有命令回复相应发送给客服端时,程序会将客服端套接字的AE_WRITABLE事件与命令回复处理器关联起来,当客服端准备好接收回复以后,就会产生AE_WRITABLE事件,引发命令回复处理器执行,执行套接字的写入操作,命令回复完以后,服务器会解除命令回复处理器与客服端套接字AE_WRITABLE时间的关联。
11.2 时间事件
Redis的时间事件有以下两种:
-
定时事件:让程序在指定的时间之后执行一次
-
周期性事件:让一段程序每隔指定时间就执行一次
一个时间事件主要由以下三个属性组成:
- id:服务器为时间事件创建的唯一ID,ID号按从小到大的顺序递增,新事件ID比旧事件的ID要大
- when:毫秒精度的UNIX时间戳,记录了时间事件的到达时间
- timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件
对于定时事件和周期性事件,时间事件处理器的返回值不一样
-
如果时间处理器返回ae.h/AE_NOMORE,那么这个事件为定时事件,该事件到达一次后就会被删除,之后不会再被执行。
-
如果返回的是一个非ae.h/AE_NOMORE的整数值,那么这个事件为周期性事件,返回这个值以后,服务器会根据时间处理器返回的值对时间事件的when属性进行更新,使得在经过返回值的毫秒值时间后能够被再次执行。
11.2.1 实现
服务器将所有的时间事件放在一个无序链表中,每当时间事件执行时,它就会遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器
注:时间按ID逆序排序(因为新事件的ID大,而新建事件采用的是头插法),无序链表指的是链表没有按时间到达快慢的顺序来排序
无序链表并不影响时间事件处理器的性能,因为正常模式下Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件。所以时间事件无序链表几乎退化成一个指针,使用它来保存时间事件并不影响事件执行的性能。
11.2.2 serverCron函数
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期,稳定地运行,这些定期操作由redis.c/serverCron函数负责执行,它的主要工作是:
- 更新服务器的各类统计信息,比如时间,内存占用,数据库占用情况等。
- 清理数据库中的过期键值对
- 关闭和清理连接失败的客服端
- 尝试进行AOF或RDB持久化操作
- 如果处于集群模式,对集群进行定期同步和连接测试
Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。
redis中可以通过修改hz选项的值来修改serverCron函数执行的频率,hz的值表示每秒执行多少次。默认为1秒数10次。
11.3 事件的执行与调度
因为服务器中同时存在文件事件与时间事件两种事件类型,所以服务器必须对两种事件进行调度,决定何时处理文件事件,何时处理时间事件,以及花多少时间来处理等等。
服务器会先遍历无序链表获得最近将执行的时间时间,计算距离这个时间事件发生要等待的时间,根据这个值设置阻塞时间,阻塞是为了等待文件事件的发生,并去处理文件事件,如果这个时间小于等于0,那么默认为0,设置阻塞的函数直接返回(不阻塞),开始执行时间事件。
- 最大阻塞时间由到达时间最接近当前的时间事件决定,这个方法既可以避免服务器对时间事件的轮询,也可以确保阻塞时间不会过长。
- 因为文件事件是随机出现的,如果等待并处理完一次文件事件后,仍然未有任何时间事件到达,你们服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会越来越逼近最近的时间事件,直到执行这个事件。
- 对文件事件和时间事件的执行都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会发生抢占,,因此不过是文件事件处理器还是时间事件处理器,都会尽可能减少阻塞时间,降低饥饿的可能性
- 因为时间事件在文件事件之后执行,并且事件之间不会发生抢占,所以时间事件的实际处理时间,通常会比时间事件的设定的到达时间晚一些