Redis 事件驱动与线程模型

22 阅读20分钟

Redis 事件驱动与线程模型:Reactor 模式与 IO 多线程

Redis 以其出色的性能著称,单机可达 10 万+ QPS。很多人好奇:Redis 是单线程的,为什么还能这么快?本文将深入剖析 Redis 的事件驱动模型、Reactor 模式、IO 多路复用技术,以及 Redis 6.0 引入的 IO 多线程优化。

📖 目录


Redis 为什么这么快?

很多人对 Redis 的高性能感到不解:

❓ 疑问:
Redis 是单线程的,为什么还能达到 10 万+ QPS?
多线程不是更快吗?

答案揭秘

  1. 纯内存操作:数据存储在内存,访问速度极快(纳秒级)
  2. 高效数据结构:精心设计的底层数据结构(SDS、Dict、SkipList)
  3. 单线程避免锁:无需线程切换和锁竞争开销
  4. IO 多路复用:一个线程处理多个客户端连接
  5. 事件驱动:高效的 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
对比表格
特性selectpollepoll
最大连接数1024无限制无限制
fd 拷贝每次每次仅注册时
查找就绪 fdO(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线程34. IO 线程并发执行 read() 读取请求
   ↓
5. 主线程等待 IO 线程完成(自旋)
   ↓
6. 主线程解析并执行命令(单线程)
   ↓
7. 主线程将写任务分配给 IO 线程
   client1 → IO线程1
   client2 → IO线程2
   client3 → IO线程38. 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:

  1. 纯内存操作(纳秒级)
  2. 高效的数据结构
  3. IO 多路复用(一个线程处理多个连接)
  4. 避免线程切换和锁竞争
  5. 瓶颈在网络 IO,不在 CPU

Q3: 什么情况下应该开启多线程?

A:

  • ✅ 高并发(1000+ 连接)
  • ✅ 大 value(> 10KB)
  • ✅ 多核 CPU(4 核以上)
  • ✅ 网络 IO 成为瓶颈

Q4: epoll 和 select 有什么区别?

A:

特性selectepoll
最大连接数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 主从复制详解:全量同步与增量同步》

敬请期待!