文章目录
- 事件驱动机制
- 文件事件
- 时间事件
- aeEventLoop(事件管理器)的具体实现
- 创建文件事件
- 事件处理
- 删除事件
事件驱动机制
Redis采用事件驱动机制来处理大量的网络IO,Redis中的事件驱动库只关注网络IO以及定时器。
事件库处理两类事件:
- 文件事件:用于处理Redis服务器和客户端之间的网络IO.
- 事件事件:Redis服务器中的一些操作需要在给定的时间点执行,而时间事件就是处理这类定时操作的。
事件驱动库的代码主要是在src/ae.c
中实现的,示意图如下:
文件事件
Redis基于Reactor模式开发了自己的网络事件处理器,也就是文件事件处理器。该处理器采用多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数。
1. 为什么单线程的Redis能那么快?
Redis的瓶颈主要在IO而不是CPU,所以为了省开发量,在6.0版本前是单线程模型;其次,Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程,但Redis的其他功能,比如持久化、异步删除、集群数据同步等,是由额外的线程执行的。
Redis采用了多路复用机制使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。
2. Redis事件响应框架ae_event及文件事件处理器
Redis自己实现了一个非常简洁的事件驱动库ae_event.
Redis使用的IO多路复用技术主要有:select、epoll、evport、kqueue
等。每个IO多路复用函数库在Redis源码中都对应一个单独的文件。Redis会根据不同的操作系统,按照不同的优先级选择多路复用技术。事件响应框架一般都采用该框架,比如netty和libevent。
文件事件处理器由四部分组成:套接字、I/O多路复用程序、文件事件分派器、事件处理器。
文件事件是对套接字操作的抽象,每当一个套接字准备好执行accept、read、write、close
等操作时,就会产生一个文件事件,因为Redis通常会连接多个套接字,所以多个文件事件有可能并发的出现。
I/O多路多路复用程序负责监听多个套接字,并向文件事件派发器传递那些产生了事件的套接字。
尽管多个文件事件可能会并发的出现,但I/O多路复用程序总是会将所有产生的套接字都放到同一个队列里边,然后文件事件处理器会以有序、同步、单个套接字的方式处理该队列中的套接字,也就是处理就绪的文件事件。
一次Redis客户端与服务器进行连接并且发送命令的过程如下:
- 客户端向服务器发起建立socket连接的请求,监听套接字将产生
AE_READABLE
事件,触发连接应答处理器执行。 - 连接应答处理器会对客户端进行应答,然后创建客户端套接字以及客户端状态,并将客户端套接字的
AE_READABLE
事件与命令请求处理器关联。 - 客户端建立连接后,向服务器发送命令,那么客户端套接字将产生
AE_READABLE
事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。 - 执行命令获得相应的命令回复,为了将命令回复传递给客户端,服务器将客户端套接字的
AE_WRITEABLE
事件与命令回复处理器关联。当客户端试图读取命令回复时,客户端套接字产生AE_WRITEABLE
事件,触发命令回复处理器将命令回复全部写入到套接字中。
3. 多路复用模型
在Redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给Redis线程处理,这就实现了一个Redis线程处理多个IO流的效果。
多路复用的Redis IO模型:
基于多路复用的Redis高性能IO模型为了在请求到达时能够通知到Redis线程,select/epoll
提供了基于事件的回调机制,针对不同事件的发生,调用相应的处理函数。
回调机制:
时间事件
select/epoll
一旦检测到FD上有请求到达时,就会触发相应的时间。这些事件会被放到一个事件队列,Redis单线程对该事件队列不断进行处理,这样一来,Redis无需一直轮询是否有请求实际发生,这就可以避免造成CPU资源浪费。同时Redis在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为Redis一直在对事件队列进行处理,所以能及时响应客户端的请求,提升Redis的响应性能。
Redis中的时间事件分为两类:
- 定时事件:让一段程序在指定的时间之后执行一次。
- 周期性时间:让一段程序每隔指定时间就执行一次。
一个时间事件是定时事件还是周期性事件取决于处理器的返回值:
如果返回值是
AE_NOMORE
,那么这个事件是一个定时事件,该事件在达到后就会删除,之后不再重复。- 如果返回值是
AE_NOMORE
的值,那么就是一个周期性事件,当一个时间事件到达后,服务器就会根据时间处理器的返回值,对时间事件的when属性进行更新,让这个事件在一定时间后再次达到。
aeEventLoop(事件管理器)的具体实现服务器的所有的时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。正常模式下的Redis服务器只使用
serverCron
一个时间事件,而在benchmark
模式下,服务器也只使用两个时间事件,所以不影响事件执行的性能。
Redis服务器端在其初始化函数initServer
中,会创建事件管理器aeEventLoop
对象。
函数aeCreateEventLoop
将创建一个事件管理器,主要是初始化aeEventLoop
的各个属性值,比如events、fired、timeEventHead和apidata
。
- 首先创建
aeEventLoop
对象。 - 初始化未就绪文件事件表、就绪文件事件表。events指向未就绪文件事件表、fired指向就绪文件事件表,表的内容在后面添加具体事件时进行初变更。
- 初始化时间事件列表,设置
timeEventHead和timeEventNextId
属性 - 调用
aeApiCreate
函数创建epoll实例,并初始化apidata
。
在这里需要补充一下关于epoll
的知识:
epoll是linux内核实现IO多路复用的一个实现,IO多路复用的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其进行读写操作。epoll监听的fd(套接字)集合是常驻内核的,他有三个系统调用(
epoll_create、epoll_wait、epoll_ctl
),通过epoll_wait
可以多次监听同一个fd集合,只返回可读写那部分。
aeApiCreate
函数的说明:
该函数首先创建了
aeApiState
对象,初始化了epoll就绪事件表;然后调用epoll_create
创建了epoll实例,最后将该aeApiState
赋值给apidata
属性(事件管理器与epoll关联)。
aeApiState
对象中的epfd存储了epoll的标识,events是一个epoll就绪事件数组,当有epoll事件发生的时候,所有发生的epoll事件和其描述符存储在这个数组中,这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。
创建文件事件
aeFileEvent
是文件事件结构,对于每一个具体的事件,都有读处理函数和协处理函数。Redis调用aeCreateFileEvent
函数针对不同的套接字的读写事件注册对应的文件事件。aeCreateFileEvent
函数主要做了三件事:
- 以fd为索引,在events未就绪事件表中找到对应的事件。
- 调用
aeApiAddEvent
函数,该事件注册到具体的底层IO多路复用中,本例为epoll(核心操作是调用epoll的epoll_ctl函数来向epoll注册响应事件) - 填充事件的回调、参数、事件类型等参数。
事件处理
因为Redis中同时存在文件事件和时间事件两个事件类型,所以服务器必须对这两个事件进行调度,决定何时处理文件事件,何时处理时间事件,以及如何调度他们。
aeMain
函数以一个无限循环不断的调用aeProcessEvents
函数来处理所有的事件。aeProcessEvents
的处理过程为:
他会首先计算距离当前时间最近的时间事件,以此计算一个超时时间;然后调用aeApiPoll
函数去等待底层的IO多路复用事件就绪;aeApiPoll
函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。
aeApiPoll
就做两件事:调用epoll_wait
阻塞等待epoll的事件就绪,超时时间就是之前根据最快达到时间事件计算而来的超时时间;然后将就绪的epoll事件转换到fired就绪事件。aeApiPoll
就是上文所说的IO多路复用程序,具体过程如下图所示:
processFileEvent
就是处理就绪文件事件的函数,也就是文件事件分派器,他其实就是遍历fired就绪事件表,然后根据对应的事件类型来调用事件中注册的不同处理器,读事件调用rfileProc
,而写事件调用wfileProc
。
processTimeEvents
是处理时间事件的函数,他会遍历aeEventLoop
的时间事件列表,如果时间事件到达就执行其timeProc
函数,并根据函数的返回值是否等于AE_NOMORE
来决定该时间事件是否是周期性事件,并修改到达时间。
删除事件
当不再需要某个事件时,需要把事件删除掉。调用aeDeleteEventLoop
函数,该函数的执行过程为:
- 根据fd在未就绪表中查找到事件
- 取消该fd对应的相应事件标识符
- 调用
aeApiFree
函数,内核会将epoll监听红黑树上的相应事件监听取消