Redis ae 事件循环

彻底搞懂 Redis 的事件驱动引擎 — 从数据结构到循环流程,从文件事件到时间事件

① 是什么 ② 数据结构 ③ 主循环动画 ④ 文件事件 ⑤ 时间事件 ⑥ 平台适配 ⑦ 完整流程 ⑧ 测一测
1 ae 是什么?
ae = "A simple event-driven programming library",是 Redis 自己实现的一个轻量事件库,代码只有约 500 行(ae.c / ae.h)。它是 Redis 整个网络 IO 的核心引擎。

🤔 为什么 Redis 要自己写事件库?

  • Libevent / libuv 太重,Redis 追求极简
  • 可以针对 Redis 场景精准优化
  • 跨平台抽象:Linux 用 epoll,macOS 用 kqueue,其他用 select
  • 整个库只管两件事:文件事件 + 时间事件

📦 ae 的职责边界

  • 监听 socket fd 的可读/可写状态
  • 到时间点时触发定时回调
  • 不断循环,直到 stop 标志被置 1
  • 不负责命令解析、数据存储、持久化

🗺️ 整体架构位置

2 核心数据结构
点击下方结构体字段可查看详细说明

① aeEventLoop — 事件循环总控

typedef struct aeEventLoop { ... }
int maxfd 当前注册的最大 fd 编号
int setsize 可监听 fd 的上限(默认 10000+)
aeFileEvent * events 📁 注册的文件事件数组,下标 = fd
aeFiredEvent * fired ⚡ 本轮就绪事件列表(epoll_wait 返回后填充)
aeTimeEvent * timeEventHead ⏰ 时间事件链表头节点
int stop 置 1 时退出主循环
void * apidata 底层 IO 多路复用的私有数据(epoll/kqueue fd 等)
aeBeforeSleepProc * beforesleep 每次阻塞前调用的钩子(如刷新输出缓冲区)
aeBeforeSleepProc * aftersleep epoll_wait 返回后立即调用的钩子

② aeFileEvent — 文件事件

typedef struct aeFileEvent { ... }
int mask AE_READABLE / AE_WRITABLE / AE_BARRIER
aeFileProc * rfileProc 可读时回调(如 readQueryFromClient)
aeFileProc * wfileProc 可写时回调(如 sendReplyToClient)
void * clientData 回调时传入的用户数据(client 指针)
每个 fd 对应一个 aeFileEvent,用数组存储(下标即 fd),O(1) 查找

③ aeTimeEvent — 时间事件

typedef struct aeTimeEvent { ... }
long long id 事件唯一 ID(单调递增)
monotime when 触发时间点(绝对时间戳,单位 ms)
aeTimeProc * timeProc 触发时的回调函数
aeEventFinalizerProc * finalizerProc 事件删除时的清理回调
struct aeTimeEvent * next / prev 双向链表指针
返回值 ≥ 0 = 毫秒后重新触发;返回 AE_NOMORE (-1) = 一次性事件,执行后删除

④ aeFiredEvent — 就绪事件(临时)

typedef struct aeFiredEvent { ... }
int fd 就绪的文件描述符
int mask 就绪类型:AE_READABLE / AE_WRITABLE
这是个临时数组,每次 epoll_wait 返回后填充,处理完当轮事件即"作废"。
3 主循环 aeMain() 动画
aeMain() 就是一个 while(!stop) 循环,不断调用 aeProcessEvents()。每一次 aeProcessEvents() 叫做一个 Tick
Tick: 0 | Phase: —
4 文件事件 — 状态机演示

事件类型

AE_READABLE fd 可读 → 有数据到来,或新连接到达

AE_WRITABLE fd 可写 → 输出缓冲区有空间,可以发送响应

AE_BARRIER 特殊标志:强制先写后读(AOF 持久化时用到)

关键 API

/* 注册事件 */
int aeCreateFileEvent(
    aeEventLoop *el,
    int fd,
    int mask,       // AE_READABLE | AE_WRITABLE
    aeFileProc *proc,
    void *clientData
);

/* 注销事件 */
void aeDeleteFileEvent(
    aeEventLoop *el, int fd, int mask
);
🖥️server 监听
🤝accept 连接
📥注册 READABLE
📖读取命令
⚙️执行命令
📤注册 WRITABLE
回包完成
点击「模拟新客户端请求」开始演示
5 时间事件 — 定时任务演示

时间事件如何确定阻塞时长?

ae 在调用 epoll_wait 时不会无限阻塞,timeout 被设置为:

timeout = 最近时间事件触发时间 - 当前时间

这样既不会错过定时任务,又能最大化利用等待时间。

Redis 中最重要的时间事件

serverCron() — 每 100ms 触发一次

  • 清理过期 Key
  • 更新 LRU 时钟
  • 持久化检查(RDB/AOF)
  • 主从复制心跳
  • 慢日志、统计信息更新

⏱️ 时间事件倒计时演示

模拟时间: 0ms
6 跨平台适配 — ae 如何封装底层 IO
ae 通过 编译时条件判断 选择底层实现,对上层提供统一 API(aeApiCreate / aeApiAddEvent / aeApiPoll
/* ae.c 中的平台选择逻辑 */
#ifdef HAVE_EVPORT
#include "ae_evport.c"   // Solaris event ports
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"  // Linux epoll  ← 最常用
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c" // macOS/BSD kqueue
        #else
        #include "ae_select.c" // fallback,fd 上限 1024
        #endif
    #endif
#endif

🐧 Linux epoll

推荐 生产环境首选,支持数十万并发连接

/* aeApiCreate:创建 epoll 实例 */
state->epfd = epoll_create(1024); // 参数已被忽略,随便写

/* aeApiAddEvent:注册 fd */
struct epoll_event ee = {0};
ee.events = EPOLLIN;              // AE_READABLE → EPOLLIN
ee.data.fd = fd;
epoll_ctl(state->epfd, EPOLL_CTL_ADD, fd, &ee);

/* aeApiPoll:等待就绪事件 */
int retval = epoll_wait(state->epfd,
    state->events, AE_SETSIZE, tvp ? tvp->tv_msec : -1);
  • 基于红黑树管理所有监听 fd,注册/删除 O(logN)
  • 就绪事件存入就绪链表,每次只返回真正就绪的 fd,O(就绪数)
  • 支持边缘触发(ET)和水平触发(LT),Redis 用 LT 更安全

🍎 macOS / BSD kqueue

macOS Redis 本地开发环境常用

/* aeApiCreate */
state->kqfd = kqueue();

/* aeApiAddEvent:注册事件 */
struct kevent ke;
EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);

/* aeApiPoll */
int retval = kevent(state->kqfd,
    NULL, 0, state->events, AE_SETSIZE, tvp);
  • 与 epoll 语义相似,但 API 风格不同
  • 支持文件、socket、信号、进程等多种 filter

⚠️ select (fallback)

不推荐生产 仅当系统无 epoll/kqueue 时使用

/* aeApiPoll */
fd_set rfds, wfds;
FD_ZERO(&rfds); FD_ZERO(&wfds);
// 遍历所有 fd,逐一加入 fd_set...
select(maxfd+1, &rfds, &wfds, NULL, tvp);
  • 每次调用都要传递完整 fd_set,O(maxfd) 复杂度
  • fd 数量上限:1024(FD_SETSIZE)
  • 性能差,不适合高并发场景
7 一次完整事件循环 Tick 的 12 步
1
aeMain() while 循环检查 stop 标志

stop == 0 → 继续;stop == 1 → Redis 关闭

2
调用 beforesleep() 钩子

刷新客户端输出缓冲区、处理 Cluster 等待回复、AOF 刷盘(如有)

3
计算 epoll_wait 的 timeout

遍历时间事件链表,找到最近触发时间 → timeout = max(0, 最近事件时间 - now)

4
调用 aeApiPoll()(即 epoll_wait)

主线程在此处阻塞,等待 fd 就绪或超时,内核把就绪 fd 填入 fired 数组

5
调用 aftersleep() 钩子

Redis 6.0+ 在此通知 IO 线程开始异步读取(多线程 IO 的入口)

6
遍历 fired 数组,处理文件事件(读)

对每个就绪 fd,若 mask & AE_READABLE → 调用 rfileProc(如 readQueryFromClient)

7
遍历 fired 数组,处理文件事件(写)

若 mask & AE_WRITABLE 且无 AE_BARRIER → 调用 wfileProc(如 sendReplyToClient)

8
AE_BARRIER 特殊处理

若文件事件设了 AE_BARRIER 标志,强制先写后读(AOF fsync 时保证持久化顺序)

9
处理时间事件(aeProcessTimeEvents)

遍历时间事件链表,对 when ≤ now 的事件调用 timeProc 回调

10
时间事件的返回值判断

返回 AE_NOMORE(-1) → 删除该事件;返回 retval ≥ 0 → when += retval,等待下次触发

11
清理已标记删除的时间事件

id == AE_DELETED_EVENT_ID 的节点从链表中摘除并释放内存

12
回到步骤 1,开始下一个 Tick

整个过程在单线程中串行执行,无锁、无竞争,这就是 Redis 简单高效的秘密

8 测一测,你学会了吗?
Q1. aeEventLoop 中的 events 数组,其下标是什么?
A 事件 ID(单调递增)
B 文件描述符 fd(数字)
C 客户端连接序号
D epoll 事件类型
Q2. epoll_wait 的超时时间由谁决定?
A 固定 100ms,与 serverCron 一致
B 永远不超时,一直阻塞
C 最近时间事件触发时间 − 当前时间
D 由客户端连接超时时间决定
Q3. 时间事件的回调返回 -1(AE_NOMORE)意味着?
A 出错了,Redis 会崩溃
B 1ms 后重新触发
C 这是一次性事件,执行后自动删除
D 暂停循环,等待下一个连接
Q4. AE_BARRIER 标志的作用是?
A 阻止新客户端连接进入
B 强制同一 fd 先处理写事件再处理读事件(先写后读)
C 给事件加互斥锁防止并发
D 限制 epoll 每次最多返回的就绪数量