Redis事件机制
核心思想:单线程为何如此高效?—— 事件驱动模型
Redis的惊人性能,尤其是在6.0版本之前的单线程模型下,主要归功于其高效的事件驱动机制。Redis的网络IO和命令读写由单个主线程处理,但这并不意味着它一次只能处理一个客户端。其核心是利用了I/O多路复用技术,使得单个线程能并发处理成千上万个网络连接,避免了在等待IO时造成的CPU资源浪费。
Redis自己实现了一个简洁的事件库ae_event,它主要处理两类事件:
- 文件事件 (File Event):处理网络IO,是性能的关键。
- 时间事件 (Time Event):处理定时任务,如周期性的
serverCron。
整个机制的核心是事件循环 (Event Loop),由aeEventLoop对象管理,它不断地循环处理已就绪的文件事件和已到期的时间事件。
一、 文件事件 (File Event) - 网络IO的核心
文件事件是Redis对套接字(Socket)操作的抽象。当一个套接字可读、可写或出现错误时,就会产生一个文件事件。
1. Reactor设计模式
Redis的文件事件处理器是基于Reactor模式实现的,其主要组件包括:
- 套接字 (Sockets):包括监听套接字和客户端连接套接字。
- I/O多路复用程序:封装了
epoll、kqueue、select等系统调用。它能同时监听多个套接字,并将产生事件的套接字放入一个就绪队列。这是实现并发的关键。 - 文件事件分派器 (Dispatcher):从就绪队列中取出套接字,根据事件类型(读/写)调用对应的事件处理器。
- 事件处理器 (Handlers):具体的业务逻辑代码,如接受新连接(
accept)、读取命令(read)、发送响应(write)等。
2. IO多路复用模型(以epoll为例)
这个模型可以类比于医院的分诊台:
- Redis主线程:就像一位专家医生,负责核心的诊断和治疗(处理命令)。
- Linux内核的
epoll:就像分诊台。它负责处理所有“杂事”,如监听所有病人(客户端连接)的到来、测温、登记(监听连接请求和数据请求)。 - 事件队列:就像分诊台前的排队队列。
- 回调机制:分诊台处理完一个病人后,会通知医生,并把病历交给医生。同样,
epoll监听到事件后,会将事件放入队列,并触发Redis注册的回调函数。
工作流程:
1. Redis主线程将所有需要监听的套接字(FDs)通过epoll_ctl注册到内核的epoll实例中。
2. 主线程调用epoll_wait,阻塞在这里等待事件发生。此时主线程不消耗CPU。
3. 当某个或某些套接字上有事件发生时(如新连接、数据到达),epoll_wait被唤醒并返回就绪的事件列表。
4. 主线程遍历就绪事件列表,根据事件类型调用预先注册好的回调函数进行处理。
5. 处理完所有就绪事件后,再次循环调用epoll_wait,等待下一批事件。
这样,Redis主线程永远在处理“已就绪”的事件,从不空等,从而实现了单线程处理高并发IO。
二、 时间事件 (Time Event) - 定时任务
时间事件用于处理定时或周期性的任务。
- 分类:
- 定时事件:在指定时间后执行一次,然后删除。
- 周期性事件:每隔指定时间就执行一次。通过事件处理器的返回值来决定是否重复(返回
AE_NOMORE则为定时事件)。
- 实现:
- 所有时间事件被存放在一个无序链表中。
- 每次事件循环时,程序会遍历此链表,检查是否有事件已到期,并执行其处理器。
- 由于Redis中的时间事件非常少(正常模式下只有
serverCron),所以遍历链表的性能开销可以忽略不计。
三、 aeEventLoop 的具体实现流程
-
创建事件管理器 (
aeCreateEventLoop)- 在服务器初始化时调用,创建一个
aeEventLoop对象。 - 初始化文件事件表(
events数组,按fd索引)和就绪事件表(fired数组)。 - 初始化时间事件链表(
timeEventHead指针)。 - 调用
aeApiCreate,在底层创建epoll实例(epoll_create),并将其与事件循环关联。
- 在服务器初始化时调用,创建一个
-
创建/注册文件事件 (
aeCreateFileEvent)- 当需要监听某个套接字
fd的读/写事件时调用此函数。 - 函数内部调用
aeApiAddEvent,最终通过epoll_ctl系统调用将fd和关心的事件(EPOLLIN/EPOLLOUT)注册到内核的epoll实例中。 - 同时,将事件的回调函数(Handler)和私有数据(
clientData)保存在events数组中对应的fd位置。
- 当需要监听某个套接字
-
事件处理循环 (
aeProcessEvents) 这是事件循环的核心,在一个while循环中被反复调用。- 计算超时时间:遍历时间事件链表,找出最快到期的那个事件,计算出距离现在还有多久。这个时间将作为
epoll_wait的超时参数。 - 等待IO事件:调用
aeApiPoll(内部调用epoll_wait),阻塞等待文件事件的发生,最大阻塞时间就是上一步计算出的超时时间。 - 处理文件事件:
epoll_wait返回后,aeApiPoll会将所有就绪的事件(fd和事件类型)填充到fired就绪事件表中。- 事件分派器遍历
fired表,根据fd在events表中找到对应的回调函数,并执行它。
- 处理时间事件:
- 调用
processTimeEvents函数,再次遍历时间事件链表。 - 执行所有已经到期的时间事件处理器。
- 根据处理器返回值,更新周期性事件的下一次执行时间,或标记一次性事件为待删除。
- 调用
- 计算超时时间:遍历时间事件链表,找出最快到期的那个事件,计算出距离现在还有多久。这个时间将作为
-
删除事件 (
aeDeleteEventLoop)- 当不再需要监听某个事件时,调用此函数。
- 它会通过
epoll_ctl从内核的epoll实例中移除对该事件的监听。
总结表
| 组件/概念 | 核心作用 | 实现细节 |
|---|---|---|
aeEventLoop |
事件循环核心,调度文件事件和时间事件。 | 包含文件事件表、就绪事件表、时间事件链表。 |
| 文件事件 | 处理网络IO,实现高并发。 | 基于Reactor模式,对socket操作的抽象。 |
| 时间事件 | 处理定时/周期性任务。 | 无序链表存储,通过遍历检查是否到期。 |
| I/O多路复用 | 并发基础,允许单线程同时监听多个IO流。 | 封装epoll等系统调用,由aeApiPoll中的epoll_wait实现阻塞等待。 |
| 文件事件分派器 | 任务分发,根据就绪事件类型调用相应处理器。 | 遍历fired就绪事件表,并执行events表中注册的回调函数。 |
aeProcessEvents |
单次循环的主体,驱动整个事件处理流程。 | 顺序执行:计算超时 -> 等待IO -> 处理文件事件 -> 处理时间事件。 |