Redis 事件驱动与线程模型:Reactor 模式与 IO 多线程
Redis 以其出色的性能著称,单机可达 10 万+ QPS。很多人好奇:Redis 是单线程的,为什么还能这么快?本文将深入剖析 Redis 的事件驱动模型、Reactor 模式、IO 多路复用技术,以及 Redis 6.0 引入的 IO 多线程优化。
📖 目录
- Redis 为什么这么快?
- 4.1 单线程模型
- 4.2 Reactor 事件驱动模型
- 4.3 IO 多路复用
- 4.4 文件事件
- 4.5 时间事件
- 4.6 事件循环流程
- 4.7 Redis 6.0 多线程
- 性能优化建议
- 常见问题解答
Redis 为什么这么快?
很多人对 Redis 的高性能感到不解:
❓ 疑问:
Redis 是单线程的,为什么还能达到 10 万+ QPS?
多线程不是更快吗?
答案揭秘:
- ✅ 纯内存操作:数据存储在内存,访问速度极快(纳秒级)
- ✅ 高效数据结构:精心设计的底层数据结构(SDS、Dict、SkipList)
- ✅ 单线程避免锁:无需线程切换和锁竞争开销
- ✅ IO 多路复用:一个线程处理多个客户端连接
- ✅ 事件驱动:高效的 Reactor 模式
性能对比:
操作类型 耗时
─────────────────────────────
L1 Cache 访问 0.5 ns
L2 Cache 访问 7 ns
内存访问 100 ns ← Redis 在这里
SSD 随机读 150 us
机械硬盘随机读 10 ms
Redis 的瓶颈不在 CPU,而在网络 IO和内存访问。
4.1 单线程模型
单线程的概念
准确的说法:
Redis 的"单线程"指的是:
• 网络 IO 和命令执行使用单线程
• 但 Redis 本身有多个后台线程
主线程(单线程):
├─ 接收客户端请求
├─ 解析命令
├─ 执行命令
└─ 返回结果
后台线程(多线程):
├─ BGSAVE(fork 子进程)
├─ BGREWRITEAOF(fork 子进程)
├─ AOF fsync(后台线程)
└─ 惰性删除(后台线程,Redis 4.0+)
单线程的优势
1️⃣ 避免上下文切换
多线程模型:
Thread 1: 运行 → 阻塞 → 运行 → 阻塞
Thread 2: 阻塞 → 运行 → 阻塞 → 运行
↑
上下文切换开销(微秒级)
单线程模型:
Thread: 运行 → 运行 → 运行 → 运行
↑
无上下文切换
性能影响:
# 上下文切换成本
线程切换:约 5-10 微秒
Redis 执行一条命令:约 0.1 微秒
# 如果频繁切换,性能反而下降
2️⃣ 无需锁机制
// 多线程需要加锁
void multiThreadSet(char *key, char *value) {
pthread_mutex_lock(&lock); // 加锁
dict_set(db, key, value);
pthread_mutex_unlock(&lock); // 解锁
}
// 单线程不需要锁
void singleThreadSet(char *key, char *value) {
dict_set(db, key, value); // 直接操作,无竞争
}
优势:
- ✅ 代码简单,易维护
- ✅ 无死锁风险
- ✅ 无锁竞争开销
3️⃣ 原子性保证
# 单线程天然保证命令的原子性
INCR counter # 读取 → 加1 → 写入(原子操作)
# 多线程需要额外机制保证原子性
4️⃣ 简化实现
// 单线程模型简洁
while (1) {
// 1. 等待事件
events = aeApiPoll();
// 2. 处理事件
for (event in events) {
processEvent(event);
}
}
// 无需考虑:
// • 线程池管理
// • 任务队列
// • 线程间通信
// • 竞态条件
单线程的局限
1️⃣ 无法利用多核 CPU
服务器配置:
• 16 核 CPU
• 64 GB 内存
Redis 单实例:
• 只使用 1 个核(6.25%)
• 其他 15 个核空闲
解决方案:
• 运行多个 Redis 实例(多端口)
• 使用 Redis Cluster(分布式)
2️⃣ 阻塞命令影响性能
# 慢命令会阻塞整个服务器
KEYS * # 遍历所有键,O(n)
FLUSHALL # 清空数据库,O(n)
SAVE # 同步保存 RDB,O(n)
# 阻塞期间,所有客户端等待
Client A: SET key1 value1 ← 等待
Client B: GET key2 ← 等待
Client C: INCR counter ← 等待
优化建议:
# 避免使用 KEYS,改用 SCAN
SCAN 0 MATCH pattern COUNT 100
# 避免使用 SAVE,改用 BGSAVE
BGSAVE
# 避免对大集合使用 SMEMBERS,改用 SSCAN
SSCAN key 0 COUNT 100
3️⃣ 单个命令执行时间长
# BigKey 操作耗时
DEL big_list # 100 万元素的 List,删除可能需要 1 秒
HGETALL big_hash # 10 万字段的 Hash,返回需要几百毫秒
# 解决方案
# 1. 避免 BigKey
# 2. 使用 UNLINK(异步删除,Redis 4.0+)
UNLINK big_list
# 3. 分批操作
LPOP big_list # 循环多次,分批删除
单线程的误解
❌ 误解 1:Redis 只有一个线程
错误理解:Redis 整个进程只有一个线程
正确理解:
• 主线程:处理客户端请求(单线程)
• 后台线程:BGSAVE、AOF 重写、异步删除等
• IO 线程:Redis 6.0+ 的 IO 多线程
❌ 误解 2:单线程性能一定差
错误理解:多线程比单线程快
正确理解:
• Redis 的瓶颈在网络 IO,不在 CPU
• 单线程避免了锁竞争和上下文切换
• 对于 IO 密集型应用,单线程 + IO 多路复用更高效
❌ 误解 3:单线程无法利用多核
错误理解:单线程只能用一个核
解决方案:
• 在一台服务器上运行多个 Redis 实例
• 使用 Redis Cluster 分布式部署
4.2 Reactor 事件驱动模型
Reactor 模式介绍
Reactor 模式是一种事件驱动的设计模式,用于处理并发 IO。
核心思想:
将 IO 操作注册到多路复用器(selector)
当 IO 就绪时,分发给相应的处理器处理
传统多线程模型 vs Reactor 模型:
传统多线程模型(Thread-Per-Connection):
┌──────────┐
│ Client 1 │─────→ Thread 1 ──→ 处理请求
└──────────┘
┌──────────┐
│ Client 2 │─────→ Thread 2 ──→ 处理请求
└──────────┘
┌──────────┐
│ Client 3 │─────→ Thread 3 ──→ 处理请求
└──────────┘
缺点:
• 1 万个连接 = 1 万个线程
• 内存占用大(每个线程约 1MB 栈空间)
• 上下文切换频繁
Reactor 模型(Event-Driven):
┌──────────┐
│ Client 1 │──┐
└──────────┘ │
┌──────────┐ │ ┌──────────────┐
│ Client 2 │──┼───→│ Reactor │──→ 单线程处理
└──────────┘ │ │ (IO 多路复用) │
┌──────────┐ │ └──────────────┘
│ Client 3 │──┘
└──────────┘
优点:
• 1 万个连接 = 1 个线程
• 内存占用小
• 无上下文切换
Redis 的 Reactor 实现
Redis 使用单 Reactor 单线程模型:
┌─────────────────────────────────────────┐
│ Redis Reactor 模型 │
└─────────────────────────────────────────┘
客户端连接
↓
┌─────────────────────┐
│ Acceptor │ 监听连接请求
│ (监听 socket) │
└──────────┬──────────┘
│ accept()
↓
┌─────────────────────┐
│ Client Sockets │ 客户端连接
│ (fd1, fd2, ...) │
└──────────┬──────────┘
│ 注册到
↓
┌─────────────────────┐
│ IO 多路复用器 │ epoll/kqueue/select
│ (Reactor) │
└──────────┬──────────┘
│ 事件就绪
↓
┌─────────────────────┐
│ Event Dispatcher │ 事件分发器
│ (aeMain) │
└──────────┬──────────┘
│ 分发事件
├────────────┬────────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 读事件 │ │ 写事件 │ │ 时间事件 │
│ Handler │ │ Handler │ │ Handler │
└──────────┘ └──────────┘ └──────────┘
事件循环机制
Redis 的事件循环(Event Loop)是 Reactor 模式的核心:
// Redis 事件循环伪代码
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 1. 处理时间事件前的回调
if (eventLoop->beforesleep != NULL) {
eventLoop->beforesleep(eventLoop);
}
// 2. 等待 IO 事件(阻塞)
aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_BEFORE_SLEEP);
}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
int processed = 0, numevents;
// 1. 计算最近的时间事件超时时间
tvp = usUntilTimer(eventLoop);
// 2. 调用 IO 多路复用,等待事件就绪
numevents = aeApiPoll(eventLoop, tvp);
// 3. 处理文件事件
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
// 读事件
if (fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
}
// 写事件
if (fe->mask & mask & AE_WRITABLE) {
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
processed++;
}
// 4. 处理时间事件
if (flags & AE_TIME_EVENTS) {
processed += processTimeEvents(eventLoop);
}
return processed;
}
事件循环流程图:
┌─────────────────────────────────────────┐
│ Redis Event Loop │
└─────────────────────────────────────────┘
开始
↓
┌─────────────────┐
│ beforesleep │ 执行睡眠前回调
│ • 处理 AOF 缓冲 │
│ • 处理客户端输出 │
└────────┬────────┘
↓
┌─────────────────┐
│ 计算超时时间 │ 查找最近的时间事件
└────────┬────────┘
↓
┌─────────────────┐
│ aeApiPoll │ IO 多路复用,等待事件
│ (epoll_wait) │ 阻塞 or 超时
└────────┬────────┘
↓
有事件就绪?
├─ 否 ──→ 跳过
└─ 是
↓
┌─────────────────┐
│ 处理文件事件 │
│ • 读事件 │
│ • 写事件 │
└────────┬────────┘
↓
┌─────────────────┐
│ 处理时间事件 │
│ • serverCron │
└────────┬────────┘
↓
stop?
├─ 否 ──→ 循环继续
└─ 是 ──→ 退出
4.3 IO 多路复用
select/poll/epoll 对比
IO 多路复用允许一个线程监控多个文件描述符(socket)。
1️⃣ select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// 使用示例
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
int ret = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
if (FD_ISSET(fd1, &readfds)) {
// fd1 可读
}
特点:
- ✅ 跨平台(所有系统都支持)
- ❌ 最大支持 1024 个文件描述符(FD_SETSIZE)
- ❌ 每次调用需要拷贝 fd_set 到内核
- ❌ 需要遍历所有 fd 检查是否就绪(O(n))
2️⃣ poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// 使用示例
struct pollfd fds[100];
fds[0].fd = fd1;
fds[0].events = POLLIN;
fds[1].fd = fd2;
fds[1].events = POLLIN;
int ret = poll(fds, 2, timeout);
if (fds[0].revents & POLLIN) {
// fd1 可读
}
特点:
- ✅ 无最大连接数限制
- ❌ 每次调用需要拷贝 pollfd 数组到内核
- ❌ 需要遍历所有 fd 检查是否就绪(O(n))
3️⃣ epoll(Linux 2.6+)
// 创建 epoll 实例
int epfd = epoll_create(1024);
// 注册事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd1;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev);
// 等待事件
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
// 处理事件
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
// events[i].data.fd 可读
}
}
特点:
- ✅ 无最大连接数限制
- ✅ 只返回就绪的 fd,无需遍历(O(1))
- ✅ 使用内核事件表,无需重复拷贝
- ✅ 支持边缘触发(ET)和水平触发(LT)
- ❌ 只支持 Linux
对比表格:
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 1024 | 无限制 | 无限制 |
| fd 拷贝 | 每次 | 每次 | 仅注册时 |
| 查找就绪 fd | O(n) 遍历 | O(n) 遍历 | O(1) 回调 |
| 跨平台 | ✅ | ✅ | ❌ (Linux only) |
| 性能 | 差 | 中 | 优 |
性能对比(1 万个连接,100 个活跃):
| IO 多路复用 | 处理时间 |
|---|---|
| select | 约 1000 次系统调用 |
| poll | 约 1000 次系统调用 |
| epoll | 约 100 次系统调用 |
Redis 的 IO 多路复用实现
Redis 对不同平台的 IO 多路复用进行了封装:
// ae.c - 根据平台选择最优实现
#ifdef HAVE_EVPORT
#include "ae_evport.c" // Solaris
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c" // Linux
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c" // BSD/macOS
#else
#include "ae_select.c" // 通用(兜底)
#endif
#endif
#endif
优先级:
1. evport (Solaris)
2. epoll (Linux) ← 最常用
3. kqueue (BSD/macOS)
4. select (通用兜底)
统一接口:
// ae.c - 统一的 API
typedef struct aeApiState {
int epfd; // epoll fd
struct epoll_event *events; // 事件数组
} aeApiState;
// 创建 IO 多路复用器
static int aeApiCreate(aeEventLoop *eventLoop);
// 调整事件表大小
static int aeApiResize(aeEventLoop *eventLoop, int setsize);
// 释放 IO 多路复用器
static void aeApiFree(aeEventLoop *eventLoop);
// 添加事件
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
// 删除事件
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask);
// 等待事件(核心)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
epoll 原理详解
epoll 的三个核心函数:
// 1. 创建 epoll 实例
int epoll_create(int size);
// 2. 管理事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// op:
// EPOLL_CTL_ADD - 添加事件
// EPOLL_CTL_MOD - 修改事件
// EPOLL_CTL_DEL - 删除事件
// 3. 等待事件
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epoll 内部结构:
┌──────────────────────────────────────┐
│ epoll 实例(内核) │
├──────────────────────────────────────┤
│ │
│ ┌────────────────────────────┐ │
│ │ 红黑树(所有监听的 fd) │ │
│ │ │ │
│ │ fd1 → events=EPOLLIN │ │
│ │ fd2 → events=EPOLLIN │ │
│ │ fd3 → events=EPOLLOUT │ │
│ │ ... │ │
│ └────────────────────────────┘ │
│ │
│ ┌────────────────────────────┐ │
│ │ 就绪队列(就绪的 fd) │ │
│ │ │ │
│ │ fd1 (可读) │ │
│ │ fd5 (可写) │ │
│ └────────────────────────────┘ │
│ │
└──────────────────────────────────────┘
工作流程:
1. epoll_ctl: 将 fd 添加到红黑树
2. 网卡收到数据 → 中断 → 内核将 fd 加入就绪队列
3. epoll_wait: 返回就绪队列中的 fd
边缘触发(ET)vs 水平触发(LT):
// 水平触发(Level Triggered,默认)
ev.events = EPOLLIN; // LT
// 边缘触发(Edge Triggered)
ev.events = EPOLLIN | EPOLLET; // ET
区别:
场景:socket 接收缓冲区有 100 字节数据
水平触发(LT):
1. epoll_wait 返回(可读)
2. read 50 字节
3. epoll_wait 再次返回(还有 50 字节未读)
4. read 50 字节
5. epoll_wait 不返回(数据读完)
边缘触发(ET):
1. epoll_wait 返回(可读)
2. read 50 字节
3. epoll_wait 不返回(状态未变化)
← 剩余 50 字节不会通知
ET 模式要求:
• 必须一次性读完所有数据
• 配合非阻塞 socket 使用
• 循环读取直到 EAGAIN
Redis 使用 LT 模式:
// ae_epoll.c
ev.events = 0;
if (mask & AE_READABLE) ev.events |= EPOLLIN;
if (mask & AE_WRITABLE) ev.events |= EPOLLOUT;
// 没有 EPOLLET,使用默认的 LT 模式
4.4 文件事件
文件事件处理器
Redis 的文件事件处理器(File Event Handler)处理所有网络 IO。
架构图:
┌──────────────────────────────────────────┐
│ 文件事件处理器 │
└──────────────────────────────────────────┘
客户端连接
↓
┌──────────┐
│ Socket 1 │
│ Socket 2 │ 注册
│ Socket 3 │─────→ ┌─────────────────┐
│ ... │ │ IO 多路复用器 │
└──────────┘ │ (epoll_wait) │
└────────┬────────┘
│ 产生事件
↓
┌─────────────────┐
│ 事件分派器 │
│ (aeMain) │
└────────┬────────┘
│ 分发
┌───────────────┼───────────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 连接应答 │ │ 命令请求 │ │ 命令回复 │
│ Handler │ │ Handler │ │ Handler │
└──────────┘ └──────────┘ └──────────┘
acceptTcp readQuery sendReply
Handler RequestHandler ToClient
事件类型
Redis 定义了两种文件事件:
// ae.h
#define AE_READABLE 1 // 读事件(socket 可读)
#define AE_WRITABLE 2 // 写事件(socket 可写)
事件处理器类型:
// 文件事件结构
typedef struct aeFileEvent {
int mask; // AE_READABLE | AE_WRITABLE
// 读事件处理器
aeFileProc *rfileProc;
// 写事件处理器
aeFileProc *wfileProc;
// 客户端数据
void *clientData;
} aeFileEvent;
事件注册与分派
1. 服务器启动,注册监听事件
// server.c - Redis 启动流程
void initServer(void) {
// 创建事件循环
server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);
// 监听端口
listenToPort(server.port, server.ipfd, &server.ipfd_count);
// 注册连接应答处理器
for (j = 0; j < server.ipfd_count; j++) {
aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler, NULL); // 接受新连接
}
}
2. 客户端连接,注册读事件
// networking.c
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
// 接受连接
int cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
// 创建客户端
client *c = createClient(cfd);
}
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
// 注册读事件处理器
if (fd != -1) {
aeCreateFileEvent(server.el, fd, AE_READABLE,
readQueryFromClient, c); // 读取命令
}
return c;
}
3. 读取客户端命令
// networking.c
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
// 读取数据
nread = read(fd, c->querybuf + qblen, readlen);
// 解析命令
processInputBuffer(c);
// 执行命令
processCommand(c);
}
4. 写回复,注册写事件
// networking.c
void addReply(client *c, robj *obj) {
// 将回复添加到输出缓冲区
prepareClientToWrite(c);
// 注册写事件(如果还没注册)
if (!(c->flags & CLIENT_PENDING_WRITE)) {
c->flags |= CLIENT_PENDING_WRITE;
listAddNodeHead(server.clients_pending_write, c);
}
}
// 在事件循环的 beforesleep 中处理
void beforeSleep(struct aeEventLoop *eventLoop) {
handleClientsWithPendingWrites();
}
int handleClientsWithPendingWrites(void) {
while ((ln = listNext(&li)) != NULL) {
client *c = listNodeValue(ln);
// 尝试直接写入(避免注册写事件)
if (writeToClient(c->fd, c, 0) == C_ERR) continue;
// 如果没写完,注册写事件
if (clientHasPendingReplies(c)) {
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c);
}
}
}
4.5 时间事件
时间事件类型
Redis 的时间事件用于执行定时任务:
// ae.h
typedef struct aeTimeEvent {
long long id; // 时间事件 ID
long when_sec; // 到达时间(秒)
long when_ms; // 到达时间(毫秒)
aeTimeProc *timeProc; // 时间事件处理器
aeEventFinalizerProc *finalizerProc; // 删除前的清理函数
void *clientData; // 客户端数据
struct aeTimeEvent *prev; // 前驱节点
struct aeTimeEvent *next; // 后继节点
} aeTimeEvent;
时间事件分类:
// 返回值决定事件类型
int timeProc(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 返回 AE_NOMORE:一次性事件(执行后删除)
return AE_NOMORE;
// 返回正整数:周期性事件(N 毫秒后再次执行)
return 100; // 100ms 后再次执行
}
serverCron 定时任务
Redis 最重要的时间事件是 serverCron,默认每 100ms 执行一次。
// server.c
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 1. 更新服务器统计信息
updateCachedTime();
// 2. 更新 LRU 时钟
server.lruclock = getLRUClock();
// 3. 检查客户端超时
clientsCron();
// 4. 检查数据库(过期键删除)
databasesCron();
// 5. 执行 BGSAVE(如果需要)
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
// 检查是否需要 RDB 保存
for (j = 0; j < server.saveparamslen; j++) {
if (server.dirty >= sp->changes &&
server.unixtime - server.lastsave > sp->seconds) {
rdbSaveBackground(server.rdb_filename);
break;
}
}
}
// 6. AOF 重写(如果需要)
if (server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled) {
rewriteAppendOnlyFileBackground();
}
// 7. AOF 缓冲区同步
flushAppendOnlyFile(0);
// 8. 关闭超时客户端
freeClientsInAsyncFreeQueue();
// 9. 复制相关
replicationCron();
// 10. 集群相关
clusterCron();
// 返回执行间隔(毫秒)
return 1000 / server.hz; // 默认 100ms
}
serverCron 执行频率:
# redis.conf
hz 10 # 每秒执行 10 次,即 100ms 一次(默认)
# 可选值:1-500
hz 100 # 每秒 100 次,即 10ms 一次(更频繁,但 CPU 占用更高)
时间事件调度
// ae.c
static int processTimeEvents(aeEventLoop *eventLoop) {
aeTimeEvent *te = eventLoop->timeEventHead;
long long maxId = eventLoop->timeEventNextId - 1;
while (te) {
// 跳过已删除的事件
if (te->id == AE_DELETED_EVENT_ID) {
te = te->next;
continue;
}
// 检查是否到达执行时间
long long now_sec, now_ms;
aeGetTime(&now_sec, &now_ms);
if (now_sec > te->when_sec ||
(now_sec == te->when_sec && now_ms >= te->when_ms)) {
// 执行时间事件处理器
int retval = te->timeProc(eventLoop, te->id, te->clientData);
// 根据返回值决定是否继续
if (retval != AE_NOMORE) {
// 周期性事件,更新下次执行时间
aeAddMillisecondsToNow(retval, &te->when_sec, &te->when_ms);
} else {
// 一次性事件,标记删除
te->id = AE_DELETED_EVENT_ID;
}
}
te = te->next;
}
return processed;
}
4.6 事件循环流程
完整流程图
┌────────────────────────────────────────────┐
│ Redis 事件循环完整流程 │
└────────────────────────────────────────────┘
启动 Redis
↓
初始化服务器
• 创建事件循环
• 监听端口
• 注册连接应答处理器
• 创建 serverCron 时间事件
↓
┌──────────────────┐
│ aeMain 开始循环 │
└────────┬─────────┘
↓
┌─────────────────────────┐
│ beforesleep 回调 │
│ • 处理 AOF 缓冲区 │
│ • 处理客户端输出缓冲区 │
│ • 清理过期键(快速模式) │
└────────┬────────────────┘
↓
┌─────────────────────────┐
│ 计算超时时间 │
│ • 查找最近的时间事件 │
│ • 计算距离现在的时间差 │
└────────┬────────────────┘
↓
┌─────────────────────────┐
│ aeApiPoll (epoll_wait) │
│ • 阻塞等待文件事件 │
│ • 或超时(处理时间事件) │
└────────┬────────────────┘
↓
有文件事件?
├─ 是
│ ↓
│ ┌─────────────────────┐
│ │ 处理文件事件 │
│ │ • 读事件(命令请求) │
│ │ • 写事件(回复客户端)│
│ └─────────┬───────────┘
│ ↓
└→ ┌─────────────────────┐
│ 处理时间事件 │
│ • serverCron │
└─────────┬───────────┘
↓
stop 标志?
├─ 否 ──→ 继续循环
└─ 是 ──→ 退出
事件优先级
Redis 处理事件的优先级:
1. beforesleep 回调(最高优先级)
↓
2. 文件事件(读事件优先于写事件)
├─ 读事件(命令请求)
└─ 写事件(命令回复)
↓
3. 时间事件(serverCron)
为什么读事件优先于写事件?
// ae.c - 先处理读事件,再处理写事件
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int fired = 0;
// 1. 先处理读事件
if (fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
fired++;
}
// 2. 再处理写事件
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
}
}
原因:
- ✅ 优先处理客户端请求(用户体验)
- ✅ 避免写回复阻塞新请求
- ✅ 读事件可能产生新的写事件
4.7 Redis 6.0 多线程
为什么引入多线程?
Redis 6.0 之前是纯单线程,但在某些场景下遇到瓶颈:
场景:10GB 数据,每次查询返回 1MB
1 万个并发连接
瓶颈分析:
• CPU:不是瓶颈(单线程足够)
• 内存:不是瓶颈(访问速度快)
• 网络 IO:是瓶颈!← 单线程处理不过来
单线程网络 IO 流程:
for (client in clients) {
read(client.socket); // 读取请求
parse(client.request); // 解析命令
execute(client.command); // 执行命令(快)
write(client.socket); // 写回复(慢)← 瓶颈
}
# 大量时间浪费在 read/write 系统调用上
解决方案:
将耗时的网络 IO 交给多个线程处理
命令执行仍然是单线程(避免锁竞争)
多线程架构
Redis 6.0 的多线程只用于网络 IO,命令执行仍是单线程:
┌──────────────────────────────────────────────┐
│ Redis 6.0 多线程架构 │
└──────────────────────────────────────────────┘
客户端请求
↓
┌──────────────────────────┐
│ IO 线程池 │
│ ┌──────┐ ┌──────┐ │
│ │IO 线程│ │IO 线程│ ... │ 并发读取
│ │ 1 │ │ 2 │ │ (read)
│ └───┬──┘ └───┬──┘ │
└──────┼─────────┼─────────┘
│ │
└────┬────┘
↓
┌──────────────────────────┐
│ 主线程 │
│ • 解析命令 │
│ • 执行命令 ← 单线程 │
│ • 生成回复 │
└──────────┬───────────────┘
↓
┌──────────────────────────┐
│ IO 线程池 │
│ ┌──────┐ ┌──────┐ │
│ │IO 线程│ │IO 线程│ ... │ 并发写入
│ │ 1 │ │ 2 │ │ (write)
│ └──────┘ └──────┘ │
└──────────────────────────┘
↓
客户端响应
工作流程:
1. 主线程 epoll_wait 等待事件
↓
2. 有客户端可读事件
↓
3. 主线程将读任务分配给 IO 线程
client1 → IO线程1
client2 → IO线程2
client3 → IO线程3
↓
4. IO 线程并发执行 read() 读取请求
↓
5. 主线程等待 IO 线程完成(自旋)
↓
6. 主线程解析并执行命令(单线程)
↓
7. 主线程将写任务分配给 IO 线程
client1 → IO线程1
client2 → IO线程2
client3 → IO线程3
↓
8. IO 线程并发执行 write() 发送回复
↓
9. 主线程等待 IO 线程完成
性能提升
基准测试(redis-benchmark):
# 单线程(Redis 5.0)
redis-benchmark -t get,set -n 1000000 -q
SET: 98765.43 requests per second
GET: 102040.82 requests per second
# 多线程(Redis 6.0,4 个 IO 线程)
redis-benchmark -t get,set -n 1000000 --threads 4 -q
SET: 145985.40 requests per second (+47%)
GET: 161290.32 requests per second (+58%)
提升场景:
- ✅ 高并发:1000+ 并发连接
- ✅ 大 value:返回值较大(> 10KB)
- ✅ 网络 IO 密集:CPU 不是瓶颈
不明显场景:
- ❌ 低并发(< 100 连接)
- ❌ 小 value(< 1KB)
- ❌ CPU 密集型操作
配置与使用
# redis.conf
# 开启多线程 IO(默认关闭)
io-threads-do-reads yes
# IO 线程数(建议设置为 CPU 核数,最多 8)
io-threads 4
# 建议:
# • 2 核:io-threads 2
# • 4 核:io-threads 3-4
# • 8 核:io-threads 6
# • 16 核:io-threads 8(最大值)
查看是否生效:
127.0.0.1:6379> INFO server
# Server
...
io_threads_active:1 # 1 表示多线程已启用
注意事项:
# 1. 不要设置太多线程
# io-threads 不宜超过 CPU 核数
# 2. 单机多实例时
# 总 IO 线程数不要超过 CPU 核数
# 例如:4 核,2 个实例,每个实例 io-threads 2
# 3. 内存不足时
# 多线程反而可能降低性能(频繁换页)
# 4. 网络带宽不足时
# 多线程无法提升性能
性能优化建议
1️⃣ 避免慢命令
# ❌ 慢命令(阻塞主线程)
KEYS * # O(n)
FLUSHALL # O(n)
FLUSHDB # O(n)
DEL bigkey # 删除大 key,O(n)
SMEMBERS bigset # 大集合,O(n)
HGETALL bighash # 大 Hash,O(n)
# ✅ 替代方案
SCAN 0 MATCH pattern # 替代 KEYS
UNLINK bigkey # 异步删除(Redis 4.0+)
SSCAN key 0 COUNT 100 # 分批获取
HSCAN key 0 COUNT 100 # 分批获取
2️⃣ 使用 Pipeline
// ❌ 逐条执行(慢)
for (int i = 0; i < 10000; i++) {
jedis.set("key" + i, "value" + i); // 1 万次网络往返
}
// ✅ 使用 Pipeline(快)
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.set("key" + i, "value" + i);
}
pipeline.sync(); // 一次性发送
3️⃣ 合理配置 hz
# redis.conf
# hz 决定 serverCron 执行频率
hz 10 # 默认值,适合大多数场景
# 高频场景(需要快速过期键删除)
hz 100 # CPU 占用会增加
# 低频场景(节省 CPU)
hz 1 # 过期键删除变慢
4️⃣ 开启多线程(适用场景)
# 高并发 + 大 value 场景
io-threads-do-reads yes
io-threads 4
# 监控效果
redis-cli INFO stats | grep instantaneous
instantaneous_ops_per_sec:150000 # QPS
5️⃣ 监控事件循环
# 查看事件循环统计
127.0.0.1:6379> INFO stats
# Stats
total_connections_received:10000
total_commands_processed:1000000
instantaneous_ops_per_sec:50000
rejected_connections:0
常见问题解答
Q1: Redis 真的是单线程吗?
A: 不完全是。
- 主线程:处理客户端请求是单线程
- 后台线程:BGSAVE、AOF 重写、异步删除等是多线程
- IO 线程:Redis 6.0+ 的网络 IO 是多线程
Q2: 为什么单线程还这么快?
A:
- 纯内存操作(纳秒级)
- 高效的数据结构
- IO 多路复用(一个线程处理多个连接)
- 避免线程切换和锁竞争
- 瓶颈在网络 IO,不在 CPU
Q3: 什么情况下应该开启多线程?
A:
- ✅ 高并发(1000+ 连接)
- ✅ 大 value(> 10KB)
- ✅ 多核 CPU(4 核以上)
- ✅ 网络 IO 成为瓶颈
Q4: epoll 和 select 有什么区别?
A:
| 特性 | select | epoll |
|---|---|---|
| 最大连接数 | 1024 | 无限制 |
| 性能 | O(n) | O(1) |
| 跨平台 | 是 | 否(Linux only) |
Q5: 如何避免阻塞命令?
A:
# 1. 避免使用
KEYS * → SCAN
DEL bigkey → UNLINK
# 2. 监控慢查询
SLOWLOG GET 10
# 3. 设置超时
redis.conf: slowlog-log-slower-than 10000 # 10ms
总结
本文深入剖析了 Redis 的事件驱动与线程模型:
单线程模型
- ✅ 优势:无锁、无切换、简单
- ❌ 局限:无法利用多核、慢命令阻塞
- 🎯 本质:主线程单线程,后台多线程
Reactor 模式
- 📐 单 Reactor 单线程
- 🔄 事件驱动,非阻塞 IO
- ⚡ 高效处理并发连接
IO 多路复用
- 🔍 select/poll/epoll 对比
- 🎯 epoll 性能最优(O(1))
- 🚀 一个线程监控多个连接
文件事件与时间事件
- 📁 文件事件:处理网络 IO
- ⏰ 时间事件:定时任务(serverCron)
- 🔁 事件循环:交替处理两种事件
Redis 6.0 多线程
- 🎯 只用于网络 IO
- ⚡ 命令执行仍是单线程
- 📈 高并发场景性能提升 50%+
理解事件驱动模型,能帮助你:
- ✅ 理解 Redis 高性能原理
- ✅ 避免阻塞命令
- ✅ 合理配置多线程
- ✅ 优化网络 IO 性能
💡 下一篇预告:《Redis 主从复制详解:全量同步与增量同步》
敬请期待!