拆解 Redis 核心引擎:I/O模型、线程模型

Redis 之所以能够支撑每秒数万甚至数十万的 QPS,绝不仅仅是因为它是基于内存操作的。更深层次的原因在于其精妙的事件驱动模型IO处理机制

本文将深入 Redis 源码层面,拆解其核心运行机制(Engine),带你理解 AeEventLoop、Reactor 模式以及 Redis 线程模型的演进之路。


1. 核心基石:Redis 的事件模型 (AeEventLoop)

Redis 是一个事件驱动程序。服务器运行的本质就是一个死循环(Event Loop),在这个循环中不断地等待事件发生、处理事件。

1.1 IO 多路复用与 Reactor 模式

Redis 并没有为每个客户端连接创建一个线程(这是传统 BIO 模型的做法),而是利用了 IO 多路复用技术(IO Multiplexing)

  • 概念:单个线程通过记录跟踪每一个 Socket(I/O流)的状态来同时管理多个 I/O 流。
  • 实现:在 Linux 下主要依赖 epoll,macOS 下依赖 kqueue。Redis 封装了一套 ae.c 事件库来屏蔽底层操作系统的差异。
  • 模式:这就是典型的 Reactor 模式。Redis 作为 Reactor(反应堆),当 Socket 可读或可写时,分发给相应的处理器(Handler)。

Redis Server (Main Thread)

Fired Events

Connect

Read

Write

SYN

CMD: GET

CMD: SET

IO Multiplexing (epoll_wait)

Event Dispatcher

Accept Handler

Read Handler

Write Handler

Client 1

Client 2

Client 3

1.2 源码透视:ae.cae_epoll.c

在 Redis 源码中,aeEventLoop 结构体是整个事件循环的核心。

核心结构体 (ae.h):

typedef struct aeEventLoop {
    int maxfd;   /* 当前注册的最大文件描述符 */
    int setsize; /* 关注的文件描述符上限 */
    aeFileEvent *events; /* 注册的文件事件 */
    aeFiredEvent *fired; /* 已触发的事件(被epoll唤醒后填充) */
    // ... 省略时间事件等
    void *apidata; /* 用于存储底层API的数据,如epoll_event结构 */
} aeEventLoop;

主循环逻辑 (ae.c):
Redis 的 main 函数最终会调用 aeMain,启动死循环。

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 如果有需要在事件循环前执行的函数(如 flushAppendOnlyFile)
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 核心:处理文件事件和时间事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

epoll 的封装 (ae_epoll.c):
aeProcessEvents 内部会调用 aeApiPoll,即 epoll_wait

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    // 调用操作系统的 epoll_wait,获取就绪的事件
    // state->events 是 struct epoll_event 数组
    int retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
            
    // ... 将内核返回的就绪事件,填充到 eventLoop->fired 数组中
    return retval;
}

2. 全景图解:一条命令的执行流程

理解了事件循环,我们来看看当客户端发送一个 SET key value 命令时,Redis 内部发生了什么。

2.1 执行步骤

  1. 建立连接:客户端发起连接,服务端 listen fd 触发读事件,调用 acceptTcpHandler 建立连接,并创建一个 client 对象,注册 readQueryFromClient 读事件处理器。
  2. 读取请求:当客户端发送数据,Socket 可读,触发 readQueryFromClient,将数据读入输入缓冲区(querybuf)。
  3. 协议解析:解析 RESP 通信协议,识别命令和参数。
  4. 命令查找:根据命令名称(如 “SET”)在命令表(Command Table)中查找对应的函数。
  5. 执行命令:调用对应的命令函数(如 setCommand),操作内存数据结构(dict)。
  6. 写入回复:将结果写入输出缓冲区(buf 或 reply 链表),并注册写事件(如果数据量大)。
  7. 发送结果:当 Socket 可写时,sendReplyToClient 将数据发送回网卡。

2.2 时序图演示

Command Execution Networking (read/write) AeEventLoop (epoll) Socket Client Command Execution Networking (read/write) AeEventLoop (epoll) Socket Client opt [输出缓冲区已满或需要在下一- 次循环发送] 发送命令 (SET k v) 触发 Socket 可读事件 调用 readQueryFromClient read() 读取数据到 buffer 解析 RESP 协议 调用 processCommand lookupCommand (查找命令表) 执行 setCommand (内存操作) addReply (结果写入输出缓冲区) 注册 Socket 可写事件 触发 Socket 可写事件 调用 sendReplyToClient 发送响应数据 (+OK)

3. 线程模型的演进:从单线程到多线程 IO

“Redis 是单线程的”这句话在 Redis 6.0 之后不再完全准确。我们需要从历史演进的角度来看待这个问题。

3.1 Redis 4.0 之前:纯粹的单线程?

实际上,Redis 一直都有后台线程(BIO Thread)。

  • 主线程:负责接收连接、解析请求、读写 Socket、执行命令、定时任务。
  • 后台线程:主要用于耗时的文件操作,如 fsync(AOF刷盘)。

3.2 Redis 4.0:Lazy Free 的引入

为了解决删除大 Key(如几百万元素的 Hash)导致主线程阻塞的问题,Redis 4.0 引入了 UNLINK 命令和 FLUSHALL ASYNC

  • 机制:将释放内存的操作通过 bio_job 扔给后台线程异步执行,主线程只做逻辑删除。

3.3 Redis 6.0:多线程 IO (Threaded I/O)

为什么引入多线程?
随着网络硬件的发展,Redis 的瓶颈逐渐从 CPU 转移到了 网络带宽网络 I/O 处理(Socket 的读写和协议解析)。

核心设计理念:
Redis 6.0 的多线程仅仅用于 网络数据的读写和协议解析,而 命令的执行(内存操作)依然是单线程的

  • 这样做的好处
    1. 无需引入复杂的锁机制,保证了数据操作的原子性。
    2. 解决了网络 IO 的瓶颈。
    3. 代码复杂度增加可控。
多线程 IO 流程图

Redis 6.0 Threading Model

IO Threads (Worker Threads)

1. Distribute Reads
1. Distribute Reads
1. Distribute Reads
2. Offload Parsing
2. Offload Parsing
2. Offload Parsing
3. Parsed Cmds
3. Parsed Cmds
3. Parsed Cmds
4. Execute Commands (Atomic)
5. Distribute Writes
5. Distribute Writes
5. Distribute Writes
6. Write to Socket
6. Write to Socket
6. Write to Socket

IO Thread 1

Client Socket 1

Main Thread

Client Socket 2

Client Socket 3

IO Thread 2

IO Thread 3

源码关键点 (networking.c):

Redis 在 beforeSleep 中处理多线程读写分配:

  • handleClientsWithPendingReadsUsingThreads: 将等待读取的客户端分配给 IO 线程。
  • handleClientsWithPendingWritesUsingThreads: 将等待写入的客户端分配给 IO 线程。
  • Busy Wait: 主线程在分配完任务后,会自旋等待所有 IO 线程完成工作,然后再统一执行命令。

4. 总结:Redis 为什么这么快?

通过对引擎的拆解,我们可以归纳出 Redis 高性能的“三驾马车”:

Redis 高性能秘密

内存操作

纯内存访问

纳秒级延迟

优秀的 IO 模型

Epoll

非阻塞 IO

Redis 6.0 多线程 IO 加持

高效的数据结构

简单动态字符串

压缩列表

跳表

单线程执行优势

避免上下文切换

无锁竞争

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TracyCoder123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值