Redis 事件驱动框架(ae.c/ae.h)深度解析
之前咱们用 “超市收银员” 的例子,简单看懂了 ae 模块是 Redis 的 “多任务神器”。现在咱们再往深走一层,不用复杂代码,只拆它的 “核心运作逻辑”—— 搞懂它怎么做到 “一个收银员(单线程)高效处理一堆顾客(多请求)”。
一、先明确:ae 模块到底要解决什么问题?
Redis 刚诞生时,有个关键需求:用单线程处理成千上万的客户端连接。如果用 “一个连接派一个线程” 的方式,线程多了会互相抢资源(比如内存、CPU),反而变慢。
ae 模块的作用,就是给单线程装上 “三头六臂”—— 让它能同时 “监听” 很多连接,哪个连接有请求就处理哪个,还能按时做定时任务(比如清理过期数据)。
二、ae 模块的 “三大核心零件”
就像一台机器有几个关键零件,ae 模块也有三个 “缺一不可” 的核心组件,咱们一个个看:
1. 零件 1:事件循环(aeEventLoop)——“工作总调度台”
你可以把它想象成收银员的 “工作台”,上面放着所有要处理的任务清单。源码里它是个结构体(简化后):
struct aeEventLoop {int maxfd; // 目前最多有多少个“顾客呼叫铃”(最大文件描述符)aeFileEvent \*events; // 所有“呼叫铃”的清单(文件事件列表)aeTimeEvent \*timeEventHead; // 所有“定时提醒”的清单(时间事件链表)int stop; // 下班开关(1=停止,0=工作)};
关键作用:
-
管理所有 “待处理任务”(不管是客户端请求,还是定时任务);
-
控制 “工作流程”:先看有没有呼叫铃响,再看有没有定时提醒到点,循环往复。
2. 零件 2:文件事件(aeFileEvent)——“顾客呼叫铃”
每个客户端连接(比如你用命令行连 Redis),在 ae 模块里都对应一个 “呼叫铃”—— 这就是文件事件。
简化结构体:
struct aeFileEvent {int mask; // 铃的类型(读铃/写铃,比如客户端发请求是“读铃”)aeFileProc \*rfileProc; // 读铃响了要做的事(比如接收客户端发的命令)aeFileProc \*wfileProc; // 写铃响了要做的事(比如给客户端返回结果)};
举个例子:
当你在命令行输入 set name zhangsan
时:
-
你的客户端连接会触发 “读铃”(mask = 读事件);
-
ae 模块会调用
rfileProc
函数,把你输入的命令接收到 Redis 里。
3. 零件 3:时间事件(aeTimeEvent)——“超市定时提醒”
Redis 里需要 “到点自动做” 的事,比如 “每 10 秒清理一次过期数据”“每天凌晨 3 点做 RDB 备份”,都靠时间事件实现。
简化结构体:
struct aeTimeEvent {long long id; // 提醒的唯一编号(避免混乱)long long when\_sec; // 到点时间(秒)long long when\_ms; // 到点时间(毫秒,更精确)aeTimeProc \*timeProc; // 到点要做的事(比如清理过期数据的函数)  struct aeTimeEvent \*next; // 下一个提醒(链表结构,按时间排序)};
关键特点:
-
所有时间事件按 “到点时间” 排序,就像你的闹钟按时间先后排好;
-
每次事件循环,只需要检查 “最早到点” 的提醒(链表头),不用遍历所有,效率高。
三、ae 模块的 “工作流程”:单线程怎么高效干活?
咱们用 “超市收银员” 的场景,再细化一遍 ae 模块的 “一天工作”—— 对应源码里的 aeMain
函数(事件循环主函数):
步骤 1:开店前准备(初始化事件循环)
-
收银员先摆好 “工作台”(创建 aeEventLoop 实例);
-
把超市大门的 “呼叫铃” 装上(注册监听端口的文件事件,比如 Redis 默认的 6379 端口);
-
设好 “定时提醒”(比如 “每 10 秒查一次过期商品”)。
步骤 2:开始营业(进入事件循环)
收银员坐在工作台前,循环做以下 3 件事:
① 看 “最早的定时提醒还有多久到点”
比如现在有两个提醒:“10 秒后清理过期商品”“1 小时后备份”,那最早的是 10 秒后。
- 这个时间决定了 “收银员最多等多久”—— 如果 10 秒内没人按呼叫铃,就等 10 秒后处理定时任务;如果期间有人按铃,就立刻处理。
② 等 “呼叫铃响”(监听文件事件)
收银员调用 aeApiPoll
函数(底层是 epoll/kqueue 等系统工具),“监听” 所有呼叫铃:
-
如果没人按铃,就等第一步算好的时间(比如 10 秒);
-
如果有人按铃(比如客户端发请求),就立刻知道是哪个铃响了(哪个客户端)、是读铃还是写铃。
③ 处理 “响了的铃” 和 “到点的提醒”
-
先处理所有 “响了的呼叫铃”(文件事件):比如客户 A 要结账(读事件),就收他的钱;客户 B 要拿商品(写事件),就给他拿。
-
再处理 “到点的定时提醒”(时间事件):比如 10 秒到了,就去清理过期商品。
步骤 3:关店(停止事件循环)
当 Redis 收到 “shutdown” 命令时,把 aeEventLoop
的 stop
设为 1,事件循环结束,收银员下班。
四、ae 模块的 “性能秘密”:为什么单线程能处理万级连接?
关键靠底层的 “IO 多路复用” 技术 —— 对应源码里的 aeApi*
系列函数(比如 Linux 下的 ae_epoll.c
)。
咱们用 “超市” 类比,解释这个技术:
-
普通方式(没有 IO 多路复用):一个顾客一个收银员,1000 个顾客要 1000 个收银员,成本高还乱;
-
IO 多路复用方式:一个收银员守着所有顾客的 “呼叫铃”,哪个响了处理哪个 —— 相当于 “一个人同时监听很多连接”,不用开多线程。
Redis 会根据操作系统自动选最优的 “监听工具”:
-
Linux 用 epoll(最常用,效率最高,支持万级连接);
-
BSD 用 kqueue(和 epoll 类似,高效);
-
Windows 用 select(效率低,支持连接少,所以 Redis 在 Windows 上很少用)。
五、动手看源码:怎么快速找到 ae 模块的核心?
如果你想自己翻源码,不用全看,找这几个关键地方就行:
-
ae.c 的 aeCreateEventLoop 函数:看事件循环怎么初始化;
-
ae.c 的 aeProcessEvents 函数:看一次事件循环的完整流程(对应咱们说的步骤 2);
-
ae.c 的 aeCreateTimeEvent 函数:看定时任务怎么创建(比如 server.c 里会调用它注册清理过期数据的任务);
-
ae_epoll.c(Linux 下):看 epoll 怎么实现 “监听多个连接”(不用看懂所有代码,看 aeApiAddEvent 怎么加事件、aeApiPoll 怎么等事件)。
总结
ae 模块其实就是 Redis 的 “多任务调度中心”:
-
靠 “事件循环” 统管所有任务;
-
靠 “文件事件” 处理客户端连接;
-
靠 “时间事件” 处理定时任务;
-
靠 “IO 多路复用” 让单线程能高效处理万级连接。
理解了 ae 模块,你就懂了 Redis 单线程高性能的核心 —— 不是靠 “多线程堆资源”,而是靠 “聪明的任务调度”。
如果还想再深入,比如看 “epoll 具体怎么监听连接”,或者 “时间事件怎么精确到毫秒”,咱们可以继续拆!