Redis网络模型 - 从fd和I/O模型到redis网络模型,再到I/O多线程,7000字长文预警

I/O模型

IO模型主要分为5类,在这里我将讲解前三类I/O模型

一、阻塞式 IO(Blocking IO)

定义(非常好理解)

阻塞 IO = 调用 IO 时,会一直等,等数据到达后才继续执行。

类似生活中的场景:

你去排队买奶茶,轮到你后,你对店员说:

"我要一杯奶茶。"

然后你不走,站在原地等奶茶做好。

在等待的时候,你 不能干别的事,只能等。


在代码层面的体现

伪代码:

c 复制代码
int n = read(fd, buf, size); // 如果没有数据,就一直卡住

如果 fd 没有数据:

  • 程序就 卡在 read 那一行
  • CPU 不执行其他任务
  • 整个线程被"阻塞"

阻塞 IO 的缺点:

  1. 线程浪费

    一个连接没数据,线程就一直被卡住。

  2. 扩展性差

    要支持 10000 个连接,就需要 10000 个线程(或多进程)。

这对于 Redis 的高性能需求来说肯定不行。


二、非阻塞式 IO(Non-blocking IO)

定义

非阻塞 IO = IO 立即返回,不等数据。

还是用奶茶店的例子:

你对店员说:
"我要奶茶。"

店员说:
"还没好,你回去等会再来问吧。"

于是你每隔几秒来问:

"好了没?好了没?好了没?"

这就是非阻塞 IO:

  • 不等数据
  • 马上返回
  • 自己不断轮询(polling)

代码表现

c 复制代码
fcntl(fd, F_SETFL, O_NONBLOCK); // 设置 fd 非阻塞

int n = read(fd, buf, size); 
if (n == -1 && errno == EWOULDBLOCK) {
    // 没数据,立即返回
}

非阻塞 IO 是"主动不断问",所以会:

  • CPU 忙等(不停调用 read)
  • 效率低
  • 浪费 CPU

这叫做 轮询(busy polling) → 性能很差。


三、IO 多路复用(IO Multiplexing)

定义

IO 多路复用 = 一个线程同时"监听"多个 socket,当哪个有数据,就告诉你处理它。

核心思想:

不要一个线程负责一个连接,而是一个线程负责很多连接。

还是奶茶店例子:


类比理解(最经典)

多个顾客(多个 socket)要取奶茶,

但你不用一个一个地问:

  • 顾客 1:奶茶好了吗?
  • 顾客 2:奶茶好了吗?

而是:

店员拿个扩音器喊:

"xxx 的奶茶好了!"

你听到后再去取。

你不再轮询所有顾客,而是由"扩音器"(内核)告诉你谁准备好了。


技术概念

Linux 提供几个事件通知机制:

IO 多路复用方式 描述
select 最古老,性能差
poll 比 select 稍好
epoll Linux 高性能 IO,多用于 Redis、Nginx

Redis 使用的是:

epoll(IO 多路复用) + 单线程事件循环


epoll 模型的核心流程

复制代码
1. 注册:告诉内核需要监听哪些 fd
2. 阻塞等待:直到某个 fd 有事件(读/写)
3. 内核返回"就绪列表"
4. 程序逐个处理

伪代码(Redis 类似):

c 复制代码
epoll_fd = epoll_create();

epoll_ctl(epoll_fd, ADD, fd1);
epoll_ctl(epoll_fd, ADD, fd2);
epoll_ctl(epoll_fd, ADD, fd3);

while (true) {
    ready = epoll_wait(epoll_fd); // 阻塞直到至少一个 fd 有事件
    for fd in ready:
        handle(fd); // 处理就绪事件
}

这就是 Redis 单线程能支撑十万连接的根本原因。


四、三者差异总结(非常关键)

下面是最容易理解的区别:

模型 行为 突出特点
阻塞 IO read 时会卡住 线程浪费
非阻塞 IO read 不等,立即返回,需要循环 CPU 浪费
IO 多路复用 epoll 统一监听所有 fd,有事件再通知 高性能、单线程可处理大量连接

总结一句话:

阻塞 IO:等
非阻塞 IO:问
IO 多路复用:内核告诉你哪个准备好了,你再去处理


五、配合图示的最终理解(超级关键)

阻塞 IO:

复制代码
read(fd)
没有数据 → 线程卡死

非阻塞 IO:

复制代码
read(fd)
没有数据 → 马上返回
程序循环不断 read → CPU 高占用

IO 多路复用:

复制代码
epoll_wait()
只是监听,不占 CPU
有数据时返回一个"就绪列表"
程序处理这些就绪的连接

redis网络模型基本概念

一、什么是 fd(文件描述符)?

fd = 文件描述符(File Descriptor)

在 Linux 里:

一切资源都被抽象成文件,包括 socket。

当 Redis 和客户端建立 TCP 连接时:

复制代码
fd = socket()

这个 fd 是一个整数,例如:

复制代码
3
12
18

表示这个连接在 Redis 进程中的"编号",用于定位内核中的 socket 资源。

换句话说:

fd 就是 Linux 给每个网络连接分配的身份证号码。

Redis 只需要记住这个数字,就能读写这条连接上的数据。


生活类比

你去医院看病会拿到一个号码:

复制代码
取号:37 号

医院不关心你是谁,只根据"编号"决定服务顺序。

fd 就是 socket 的编号


二、什么是 epoll_wait?

epoll_wait 属于 Linux 的 IO 多路复用 API

Redis 在启动时会创建一个 epoll 句柄:

c 复制代码
epfd = epoll_create()

然后把所有客户端连接 fd 加进去,让 epoll 监控它们:

c 复制代码
epoll_ctl(epfd, ADD, client_fd)

之后 Redis 会调用:

c 复制代码
epoll_wait(epfd, ...)

作用就是:

阻塞等待,看哪些 fd 上有事件(读/写)发生。

如果没有 fd 准备好,Redis 会睡眠,不占 CPU。


生活类比:排队取餐

你告诉店员:

"我点了 3 份餐,当我哪个餐做好时,叫我。"

店员(内核)会:

  • 盯着你的 3 份餐(对应多个 fd)
  • 哪份先做完,就直接通知你(epoll_wait 返回)

你无需每秒自己跑去问:

"好了没?好了没?好了没?"

这就是 epoll 的意义。


三、为什么客户端发送命令后,阻塞在 epoll_wait 的 Redis 会立刻醒来?

现在我们讲最关键的部分:

为什么客户端一发送命令,epoll_wait 就返回 fd 可读事件?

因为 epoll_wait 监控的是 socket 的 数据缓冲区状态

当客户端通过 TCP 发送数据(命令)时:

复制代码
数据进入内核 socket 的接收缓冲区
↓
内核将此 socket 标记为"可读"
↓
内核把这个事件加入 epoll 的就绪队列
↓
epoll_wait 被唤醒并返回这个 fd

所以 epoll_wait 并不神奇:

  • 它不主动轮询
  • 它只是内核的"通知机制"
  • 当 socket 数据到了 → 内核叫醒 epoll_wait

再用生活例子解释一次

你坐在候餐区(Redis 在执行 epoll_wait)。

厨房(内核)在监控你的订单状态(socket)。

当你的餐做好了(客户端发送命令 → 数据到达内核):

  • 不需要你盯着
  • 厨房自动通过叫号器(epoll)通知你
  • 你从等待状态醒来(epoll_wait 返回)

这就是为什么 Redis 不需要轮询,却能高性能处理成千上万连接。


四、整个流程图

复制代码
客户端发送命令
↓
数据到达服务器内核的 socket 缓冲区
↓
内核把 fd 标记为 READABLE
↓
epoll 内部的就绪链表加入这个 fd
↓
正在阻塞的 epoll_wait 立即返回
↓
Redis 主线程被唤醒
↓
调用 readQueryFromClient 读取命令
↓
执行命令 → 写结果 → 写事件

再加一个图示:

复制代码
client ---> kernel socket buffer ---> epoll ---> Redis

每一步都有内核参与处理。


Redis 网络模型核心

一、Redis 主线程的整体结构(单线程 Reactor 模型)

Redis 的网络处理模型可以归纳为一句话:

Redis 使用单线程 + IO 多路复用(epoll),主线程统一处理所有客户端的读写事件与命令执行。

Redis 的主线程中有一个事件循环:

复制代码
aeEventLoop

它负责:

  • 监听客户端 fd(socket)
  • 处理读事件(读客户端命令)
  • 执行命令(SET/GET/ZADD/...)
  • 处理写事件(将结果返回客户端)
  • 执行定时任务(如过期键清理)

主循环伪代码(简化):

c 复制代码
while (!stop) {
    int num = epoll_wait(监听所有FD);
    for each ready_fd:
        调用相应的事件处理器(读 or 写)
    
    执行时间事件(定时清理、过期处理等)
}

二、Redis 如何处理"读事件"(客户端发来命令)

1)客户端发送命令 → fd 变为"可读"

例如客户端发送:

复制代码
SET name jack

Linux 内核会把事件写入 epoll 的就绪列表:

复制代码
fd 可读 → epoll_wait 返回

2)Redis 调用 readQueryFromClient() 读取数据

处理器代码:

c 复制代码
readQueryFromClient()

Redis 从 socket 读取数据:

  • 读入 querybuf(命令缓冲区)
  • 支持管道(pipeline)
  • 一次读多个命令

伪代码:

c 复制代码
nread = read(fd, client->querybuf)

如果读到数据 → 进入下一阶段

如果读到 0(连接关闭) → 关闭客户端

如果出错 → 删除客户端


3)Redis 将命令解析(协议解析)

Redis 使用 RESP 协议,比如:

复制代码
*3
$3
SET
$4
name
$4
jack

Redis 使用:

复制代码
processInputBuffer()

把 querybuf 中的命令解析成:

复制代码
argv[] = ["SET", "name", "jack"]
argc   = 3

4)命令分发(查命令表)

Redis 有一个命令表 commandTable:

复制代码
SET → setCommand()
GET → getCommand()
ZADD → zaddCommand()

解析后调用对应的函数。

伪代码:

c 复制代码
cmd = lookupCommand(argv[0])
cmd->proc(client)   // 执行命令

5)执行命令(真正的业务逻辑)

例如:

复制代码
SET key val

Redis 会:

  • 计算 key 的哈希
  • 修改 dict 中的内容
  • 更新过期时间
  • propagate 到 AOF / Replication
  • 标记脏数据 dirty++

执行完成后,将结果写入 client->buf(写缓冲区)

例如:

复制代码
+OK

6)为客户端注册写事件

当写缓冲区不为空时:

复制代码
aeCreateFileEvent(loop, fd, AE_WRITABLE, sendReplyToClient)

➡ 意味着:等这个 fd 能写时,我就写回结果

读事件流程结束。


三、Redis 如何处理"写事件"(返回命令结果)

1)fd 可写时,触发写事件处理器

处理器:

复制代码
sendReplyToClient()

Redis 会像流水线一样从写缓冲区取数据:

c 复制代码
write(fd, client->buf)

如果一次写不完:

  • 缓冲区未清空
  • 下次继续触发写事件

2)写完后取消写事件监听(非常关键)

当缓冲区全部写完:

复制代码
aeDeleteFileEvent(loop, fd, AE_WRITABLE)

为什么要删除?

因为:

  • 可写事件几乎 ALWAYS 触发
  • 不删除会导致 CPU 100% 空转

Redis 非常聪明:

写事件只在有数据要写时才监听。


四、整体流转图(最重要)

复制代码
         客户端
            |
       发送命令(SET)
            |
         fd 可读
            ↓
      epoll_wait 返回
            ↓
    readQueryFromClient
            ↓
     processInputBuffer
            ↓
       命令执行(SET)
            ↓
      结果写入 client->buf
            ↓
     注册写事件 AE_WRITABLE
            ↓
     epoll_wait 等待 fd 可写
            ↓
    sendReplyToClient
            ↓
     清理缓冲区,删除写事件
            ↓
         返回结果给客户端

Redis 把每一步做到极致,所以性能极高。


五、配合"单线程"理解 Redis 高性能

即便 Redis 单线程,也能处理大量请求,因为:

  • epoll_wait 过滤掉无意义 IO
  • 所有命令都是内存操作(快)
  • 写事件只在需要时监听
  • Redis 避免锁竞争(单线程无锁)

所以 Redis 每秒可以处理十万级请求。


大白话理解

首先客户端和redis服务器建立socket链接,redis使用fd来描述该socket链接;同时在redis服务器有一个不断循环的事件aeEventLoop监听该socket是否发送命令,epoll_wait就在aeEventLoop不断循环;当客户端发送了一个命令之后,fd 变为可读,表示客户端发来命令,epoll_wait监听到fd可读后,就使用readQueryFromClient()来读取socket中发来的命令,使用processInputBuffer()来解析命令,lookupCommand()和call()来执行命令,执行完之后,将fd注册为可写,epoll_wait监听到fd可读后,返回结果给客户端。

多线程I/O

  • Redis 6.0 的 IO 多线程是怎么加入主线程模型的?

Redis 一直以 单线程 著称,但从 Redis 6.0 开始,加入了 多线程 IO(仅用于网络读写),以解决高并发场景下网络瓶颈问题。

但注意:


核心结论(记住)

Redis 6.0 的多线程只用于 IO(网络读 & 网络写),不用于命令执行。
主线程依然负责命令执行(逻辑处理)。

即:

多线程 = 提升"收数据 / 发数据"的吞吐

单线程 = 保持"命令执行"的原子性与简单性


一、为什么 Redis 要引入 IO 多线程?

Redis 单线程瓶颈不是 CPU(CPU 处理内存操作很快)

真正的瓶颈是:

大量客户端接入时的网络读写(read/write 系统调用)

read/write 会消耗大量时间,尤其是:

  • pipeline 批量命令
  • 大量客户端同时读写
  • 高 QPS 网络流量

因此 Redis 6.0 把网络层(读取请求、发送响应)改成多线程加速。


二、Redis 6.0 网络模型:单线程 + 多线程(混合模型)

完整结构如下:

复制代码
            ┌───────────────┐
            │   主线程       │
            │  epoll_wait    │
            └───────┬────────┘
                    │
                    ▼
           fd 可读事件到来
                    │
            ┌───────┴────────┐
            │ 多线程读取请求  │   ← 6.0 新增(并发 read)
            └───────┬────────┘
                    │
                    ▼
           主线程执行命令(单线程)
                    │
            ┌───────┴────────┐
            │ 多线程发送响应  │   ← 6.0 新增(并发 write)
            └───────┬────────┘
                    │
                    ▼
           epoll_wait 下一轮循环

主线程永远做:

  • epoll_wait(事件监听)
  • 解析命令调用执行函数
  • 操作内存数据结构(SET/GET/ZADD...)
  • 复制/持久化等内部逻辑

多线程只做两件事:

  1. read 请求:把 socket 的数据读到 querybuf
  2. write 响应:把 reply 写回 socket

三、Redis 6.0 IO 多线程的工作流程(详细图解)

假设有客户端发送 10 万条命令,流程如下:


① epoll_wait 监听到几个 fd 可读(主线程)

主线程做:

复制代码
collect clients with readable data

把这些"准备好读数据的客户端"记录下来:

复制代码
server.clients_pending_read

然后把这些 client 分配给 IO 线程。


② 多线程开始读数据(读取 querybuf)

线程池中的每个线程负责一部分 client:

复制代码
线程 1 → 处理 client1、client2、client3
线程 2 → 处理 client4、client5
线程 3 → 处理 client6、client7 ...

线程执行函数:

c 复制代码
readQueryFromClientMultiThreaded()

任务:

  • read() 从 socket 读取数据
  • 填充 client->querybuf

但注意:

命令解析 NOT 在这里做

因为命令解析会修改 client 状态,不适合多线程。


③ 多线程读完 → 主线程开始解析命令 & 执行命令

主线程开始处理所有"读完数据的 client":

复制代码
processInputBuffer()
lookupCommand()
call()

这一部分仍是:

单线程
不加锁
保证命令原子性
保证 Lua、事务、键空间操作一致性

保持 Redis 传统优势。


④ 命令执行结束 → 主线程把准备写的 client 发给 IO 线程池

Redis 主线程在执行完命令后,把写缓冲区中有数据的客户端放入:

复制代码
server.clients_pending_write

并交给 IO 线程。


⑤ 多线程并发 write 回客户端

IO 线程执行:

c 复制代码
sendReplyToClientMultiThreaded()

任务:

  • write() 写 socket
  • 清空出站缓冲区

减少主线程在 write 上的等待时间。


⑥ 主线程继续下一轮事件循环

复制代码
epoll_wait

等待下一批命令。


四、多线程的数量如何配置?

在 redis.conf 中:

复制代码
io-threads 4

默认是 1(即不开启多线程)。

一般推荐:

复制代码
CPU 核心数 - 2

例如 8 核:

复制代码
io-threads 6

五、Redis 6.0 IO 多线程的实现原则(非常重要)

Redis 做到:

不改变原有"命令执行单线程"

保持数据结构的无锁访问、超高性能。

多线程只处理"耗时且可并行"的 read/write

网络 IO 占用 CPU 多 → 可多线程并行。

不引入锁竞争

中间环节 carefully 设计,尽量避免锁。


六、为什么 Redis 不把命令执行也多线程化?

因为 Redis 的命令执行涉及:

  • dict(哈希表)操作
  • skiplist
  • quicklist
  • 过期策略
  • 主从复制
  • AOF/RDB
  • 键空间修改事件

这些结构都是无锁设计的,一旦多线程执行:

  • 需要给每个数据结构加锁
  • 性能会大幅下降
  • 代码复杂度增加 10 倍
  • 兼容性会变差(Lua、事务、脚本等)

Redis 采取的路线是:

用多线程扩展 IO 吞吐,而不是破坏核心的"单线程内存操作"优势。


七、Redis 6.0 IO 多线程完整流程图(建议收藏)

复制代码
┌─────────────────────────┐
│       主线程 aeLoop      │
│      epoll_wait监听事件  │
└───────────┬──────────────┘
            │ fd可读
            ▼
   ┌──────────────────┐
   │  IO线程读取socket │  ←  多线程读(read)
   └───────────┬────────┘
               │
               ▼
   ┌──────────────────┐
   │ 主线程解析+执行命令 │ ← 单线程执行命令
   └───────────┬────────┘
               │
               ▼
   ┌──────────────────┐
   │  IO线程写回客户端 │ ← 多线程写(write)
   └───────────┬────────┘
               │
               ▼
        回到 aeEventLoop

八、总结

Redis 6 的 IO 多线程流程:

  1. 主线程 epoll_wait 得到可读 fd
  2. 多个 IO 线程并发 read(读取客户端命令)
  3. 主线程解析命令 + 执行命令(单线程)
  4. 多个 IO 线程并发 write(返回结果)
  5. 主线程继续进入 epoll_wait

核心思想:

多线程加速网络 IO;单线程保障逻辑执行的一致性与高性能。

相关推荐
w***i2941 小时前
【SQL】count(1)、count() 与 count(列名) 的区别
数据库·sql
Hui Baby1 小时前
mysql的自定义HINT语法-实战
数据库·mysql·adb
init_23611 小时前
【BGP入门专题-5】bgp路由反射器RR
运维·网络
一 乐1 小时前
鲜花销售|基于springboot+vue的鲜花销售系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
youmdt1 小时前
mysql-存储引擎
数据库·mysql
where happens1 小时前
SQL Server 收缩日志
数据库·sql·oracle
w***i2941 小时前
SQL Server 创建用户并授权
数据库·oracle
韩立学长1 小时前
基于Springboot儿童福利院规划管理系统o292y1v8(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
龙仔7251 小时前
如何通过两台服务器完成六个节点的redis缓存。Redis Cluster(3主3从)完整部署文档
数据库·redis·缓存