Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Redis核心数据结构
  • Redis持久化机制
  • Redis高可用架构
  • Redis分布式锁
  • Redis缓存设计
  • Redis大Key与热Key
  • Redis限流
  • Redis IO多路复用
    • 一、数据传输的基石:缓冲区机制
      • 1.1 内核缓冲区:操作系统的 IO 中转站
      • 核心特性
      • 核心作用
      • 1.2 应用缓冲区:Redis 自主管理的用户态内存
      • 分类与设计
      • 核心作用
      • 1.3 缓冲区交互:数据从客户端到 Redis 的完整流转
      • 阶段 1:命令接收(读数据)
      • 阶段 2:响应发送(写数据)
    • 二、事件驱动模型:Redis 高并发的底层架构
      • 2.1 事件的两种核心类型
      • (1)文件事件(File Event)
      • (2)时间事件(Time Event)
      • 2.2 Reactor 模式的核心组件
      • (1)事件循环(aeEventLoop)
      • (2)事件源(aeFileEvent / aeTimeEvent)
      • (3)事件处理器(回调函数)
    • 三、文件事件:网络 IO 的高效处理机制
      • 3.1 IO 多路复用:单线程处理多连接的关键
      • 3.1.1 三种 IO 模型的对比
      • 3.1.2 Redis 支持的 IO 多路复用技术
      • 3.1.3 epoll 为何是 Linux 下的首选?
      • 3.2 文件事件的完整处理流程(以客户端请求为例)
      • 步骤 1:监听连接事件(服务器启动阶段)
      • 步骤 2:建立客户端连接
      • 步骤 3:读取并解析命令
      • 步骤 4:发送响应结果
      • 3.3 事件优先级与优化策略
    • 四、时间事件:后台任务的调度机制
      • 4.1 时间事件的两种类型
      • 4.2 核心周期性任务:serverCron 函数
      • 4.3 时间事件的实现逻辑
    • 五、事件循环:Redis 主线程的运行逻辑
      • 详细步骤解析
    • 总结:Redis 高性能的设计哲学
  • Redis过期删除策略
  • Redis Bitmap
  • 《Redis》笔记
Tavio
2023-06-01
目录

Redis IO多路复用

Redis 作为高性能键值数据库,以单线程架构支撑数万级 QPS 的高并发场景,其核心奥秘在于事件驱动模型与IO 多路复用技术的深度协同。

# 一、数据传输的基石:缓冲区机制

在 Redis 与客户端的网络交互中,数据并非直接在应用程序与网卡间传输,而是通过内核缓冲区(内核态)与应用缓冲区(用户态)的协作完成。

# 1.1 内核缓冲区:操作系统的 IO 中转站

内核缓冲区是操作系统为每个网络套接字(Socket)分配的内核态内存区域,是数据在“网卡与应用程序”之间的必经之路。

# 核心特性

  • 归属与访问:属于操作系统内核管理,用户进程(如 Redis)无法直接读写,必须通过 read()、write() 等系统调用间接访问(需切换至内核态)。
  • 大小控制:由系统内核参数配置(如 Linux 的 net.core.rmem_default 控制读缓冲区默认大小,net.core.wmem_default 控制写缓冲区),通常为几 KB 到几十 KB。
  • 双缓冲区设计:每个 Socket 包含读缓冲区(recv buffer)和写缓冲区(send buffer),分别负责接收和发送数据。

# 核心作用

  1. 解耦异步传输:
    客户端发送的数据先由网卡接收并拷贝至内核读缓冲区,即使 Redis 未及时处理,数据也不会丢失;Redis 发送响应时,只需将数据写入内核写缓冲区,内核会异步将数据发送至网卡,无需 Redis 等待。

  2. 优化系统调用:
    内核缓冲区会暂存小批量数据(如 TCP 的 Nagle 算法合并小数据包),减少应用程序频繁调用 read()/write() 的开销(每次系统调用需切换内核态,成本较高)。

  3. 平衡硬件速度差异:
    网卡数据传输速度(GB 级/秒)远高于用户进程处理速度(MB 级/秒),缓冲区可暂存突发数据,避免“高速设备等待低速设备”的性能浪费。

# 1.2 应用缓冲区:Redis 自主管理的用户态内存

应用缓冲区是 Redis 在用户态内存中为每个客户端连接分配的内存区域,由 Redis 直接管理,无需内核介入即可读写。

# 分类与设计

Redis 为每个 redisClient 结构体维护两类应用缓冲区:

缓冲区类型 存储内容 数据结构 大小特性
输入缓冲区(Input Buffer) 客户端发送的命令数据(如 SET key value) querybuf 字段(动态字符串) 初始 16KB,动态扩容,最大不超过 1GB(可通过 client-query-buffer-limit 配置)
输出缓冲区(Output Buffer) 待发送给客户端的响应数据(如 OK、value 值) 由 buf(固定 16KB 小缓冲区)和 reply(动态链表/字符串)组成 小响应存 buf,大响应(如批量查询结果)存 reply,可配置上限避免内存溢出

# 核心作用

  1. 支撑命令解析:
    输入缓冲区暂存完整的命令数据,Redis 可基于缓冲区逐字节解析 Redis 协议,避免“边读边解析”的复杂逻辑。

  2. 缓存响应结果:
    输出缓冲区暂存命令执行结果,等待内核写缓冲区空闲时批量发送,减少系统调用次数(例如,一次发送多个小响应,替代多次单独发送)。

  3. 避免阻塞单线程:
    Redis 单线程处理命令时,输出缓冲区可暂存响应数据,无需等待客户端接收完成即可处理下一个命令,提升并发效率。

# 1.3 缓冲区交互:数据从客户端到 Redis 的完整流转

以“客户端发送 SET key value 命令并接收 OK 响应”为例,数据在缓冲区之间的流转流程如下:

# 阶段 1:命令接收(读数据)

  1. 客户端调用 send() 发送命令,数据从“客户端应用缓冲区”→“客户端内核写缓冲区”→ 网卡。
  2. Redis 服务器网卡接收数据,内核将数据从网卡拷贝至“Redis 内核读缓冲区”。
  3. IO 多路复用器(如 epoll)检测到该 Socket 读事件就绪,通知 Redis 主线程。
  4. Redis 调用 read() 系统调用,将数据从“内核读缓冲区”拷贝至“Redis 输入缓冲区”。
  5. Redis 解析输入缓冲区中的命令,执行 SET 操作,将响应 OK 写入“输出缓冲区”。

# 阶段 2:响应发送(写数据)

  1. Redis 向 IO 多路复用器注册该客户端 Socket 的写事件。
  2. 内核写缓冲区空闲时(有空间存储新数据),IO 多路复用器通知写事件就绪。
  3. Redis 调用 write() 系统调用,将数据从“输出缓冲区”拷贝至“内核写缓冲区”。
  4. 内核异步将数据从“内核写缓冲区”拷贝至网卡,发送给客户端。
  5. 若响应数据未完全发送(如数据过大),保留未发送部分至下次写事件触发;若发送完成,取消写事件注册并清空输出缓冲区。

核心要点:数据传输需经过两次关键拷贝(内核缓冲区 ↔ 应用缓冲区),IO 多路复用技术仅监听内核缓冲区的就绪状态,避免 Redis 对未就绪 Socket 进行无效系统调用。

# 二、事件驱动模型:Redis 高并发的底层架构

Redis 是典型的事件驱动型程序,其核心遵循 Reactor 模式(反应器模式):由一个主线程通过 IO 多路复用机制监听多个事件源,当事件就绪时触发对应回调函数处理。这种设计既避免了多线程上下文切换的开销,又保证了单线程下的有序执行。

# 2.1 事件的两种核心类型

Redis 将所有待处理事件分为两类,统一由事件循环调度:

# (1)文件事件(File Event)

与网络 IO 相关的事件,是处理客户端请求的核心,包括:

  • 连接事件:客户端发起连接(accept)、断开连接。
  • 读写事件:客户端发送命令(读)、Redis 发送响应(写)。

# (2)时间事件(Time Event)

与定时/周期性任务相关的事件,用于维护服务器状态,包括:

  • 定时事件:仅执行一次(如 UNLINK 命令异步删除大键)。
  • 周期性事件:重复执行(如后台过期键清理、持久化检查)。

# 2.2 Reactor 模式的核心组件

Redis 通过 ae.c 模块实现 Reactor 模式,核心组件如下:

# (1)事件循环(aeEventLoop)

Reactor 模式的“大脑”,负责管理所有注册的事件、监听事件就绪状态、触发回调函数。其核心结构定义如下:

typedef struct aeEventLoop {
    int maxfd;                     // 已注册事件的最大文件描述符(FD)
    int setsize;                   // 事件集合大小(支持的最大 FD 数量)
    long long timeEventNextId;     // 下一个时间事件的唯一 ID
    aeFileEvent *events;           // 注册的文件事件(FD 到事件的映射表)
    aeFiredEvent *fired;           // 已触发的事件列表(IO 多路复用返回的就绪事件)
    aeTimeEvent *timeEventHead;    // 时间事件链表(无序存储)
    int stop;                      // 事件循环停止标志(0:运行,1:停止)
    void *apidata;                 // 多路复用技术的私有数据(如 epoll 的句柄)
    aeBeforeSleepProc *beforesleep;// 事件循环阻塞前的回调函数(如 serverCron 预热)
} aeEventLoop;
1
2
3
4
5
6
7
8
9
10
11

# (2)事件源(aeFileEvent / aeTimeEvent)

  • 文件事件(aeFileEvent):绑定特定 FD 和事件类型(读/写),包含事件就绪时的回调函数。
    typedef struct aeFileEvent {
        int mask;                     // 事件类型掩码(AE_READABLE / AE_WRITABLE)
        aeFileProc *rfileProc;        // 读事件回调函数(如 readQueryFromClient)
        aeFileProc *wfileProc;        // 写事件回调函数(如 sendReplyToClient)
        void *clientData;             // 私有数据(如 redisClient 指针)
    } aeFileEvent;
    
    1
    2
    3
    4
    5
    6
  • 时间事件(aeTimeEvent):包含事件执行时间、回调函数、是否周期性执行等信息。

# (3)事件处理器(回调函数)

针对不同事件的处理逻辑,例如:

  • acceptTcpHandler:处理新客户端连接。
  • readQueryFromClient:从客户端读取并解析命令。
  • sendReplyToClient:向客户端发送响应。
  • serverCron:执行周期性后台任务。

# 三、文件事件:网络 IO 的高效处理机制

文件事件是 Redis 处理网络请求的核心,本质是对 Socket 操作的抽象。通过 IO 多路复用技术,Redis 单线程可同时监听数千个客户端连接的读写事件,实现高并发处理。

# 3.1 IO 多路复用:单线程处理多连接的关键

IO 多路复用的核心思想是:一个线程通过一次系统调用(如 epoll_wait)监听多个 FD,当某个 FD 就绪(可读/可写)时,系统调用返回并通知该 FD 就绪,再触发对应处理逻辑。

# 3.1.1 三种 IO 模型的对比

IO 模型 核心逻辑 阻塞点 并发能力 典型场景
阻塞 IO(BIO) 调用 read() 后阻塞至数据拷贝完成 等待数据 + 数据拷贝 极低 简单本地文件读写
非阻塞 IO(NIO) 调用 read() 后立即返回,无数据则轮询重试 仅数据拷贝 一般 低延迟、小数据场景
IO 多路复用 内核监听多 FD,仅通知就绪事件 无事件时阻塞在监听阶段 极高 高并发网络服务(Redis)

为什么 Redis 不选非阻塞 IO?
非阻塞 IO 需要应用程序主动轮询所有 FD 是否就绪(空轮询),即使无事件也会消耗 CPU;而 IO 多路复用由内核监听,无事件时线程阻塞,几乎不消耗 CPU。

# 3.1.2 Redis 支持的 IO 多路复用技术

Redis 为跨平台适配,实现了多种 IO 多路复用方案,通过统一接口封装(优先选择高效方案):

技术 适用系统 核心优势 Redis 优先级
epoll Linux 事件驱动,支持水平触发(LT)/边缘触发(ET),FD 无上限,事件查询效率 O(1) 最高
kqueue Mac OS/FreeBSD 类似 epoll,支持 ET 模式,FD 无上限,适合高并发 次之
select 所有系统 兼容性好,但 FD 上限 1024,事件查询 O(n)(遍历所有 FD),效率低 最低
poll 大多数系统 突破 FD 上限,但仍需遍历所有 FD,效率低于 epoll/kqueue 较低

# 3.1.3 epoll 为何是 Linux 下的首选?

  1. 无 FD 上限:
    select 受限于 FD_SETSIZE(默认 1024),而 epoll 支持的 FD 数量由系统内存决定(通常可达百万级),适合 Redis 高并发场景。

  2. 事件通知机制更高效:

    • select 每次调用需将 FD 集合从用户态拷贝到内核态,并遍历所有 FD 检查就绪状态(O(n) 复杂度)。
    • epoll 用红黑树存储 FD,注册时拷贝一次,后续无需重复拷贝;事件就绪时通过回调直接通知(O(1) 复杂度)。
  3. 灵活的触发模式:

    • 水平触发(LT,Redis 默认):FD 有未读数据时持续通知,实现简单,避免漏处理。
    • 边缘触发(ET):仅在 FD 状态变化时通知一次,需一次性读完所有数据,效率更高但实现复杂(Redis 为稳定性选择 LT)。

# 3.2 文件事件的完整处理流程(以客户端请求为例)

以“客户端发送 SET key value 命令”为例,解析文件事件从连接建立到响应发送的全生命周期:

# 步骤 1:监听连接事件(服务器启动阶段)

  • Redis 启动时创建监听套接字(绑定 6379 端口),向 epoll 注册读事件(AE_READABLE),回调函数为 acceptTcpHandler(负责处理新连接)。

# 步骤 2:建立客户端连接

  • 客户端发起 TCP 三次握手,连接建立后,内核将监听 FD 标记为“读就绪”,epoll 将其加入“已触发事件列表”。
  • Redis 主线程调用 acceptTcpHandler,通过 accept() 系统调用获取客户端 Socket,并创建 redisClient 对象(初始化输入/输出缓冲区)。
  • 向 epoll 注册该客户端 FD 的读事件,回调函数为 readQueryFromClient(负责读取命令)。

# 步骤 3:读取并解析命令

  • 客户端发送 SET key value 命令,内核将数据写入客户端 FD 的读缓冲区,epoll 检测到“读就绪”并通知 Redis。
  • Redis 调用 readQueryFromClient,通过 read() 将数据从内核读缓冲区拷贝至 Redis 输入缓冲区。
  • 解析输入缓冲区中的 Redis 协议,提取命令参数(SET、key、value),执行命令并将结果(OK)写入输出缓冲区。

# 步骤 4:发送响应结果

  • Redis 向 epoll 注册该客户端 FD 的写事件(AE_WRITABLE),回调函数为 sendReplyToClient(负责发送响应)。
  • 内核写缓冲区空闲时(有空间存储数据),epoll 通知“写就绪”。
  • Redis 调用 sendReplyToClient,通过 write() 将输出缓冲区中的 OK 拷贝至内核写缓冲区,内核异步发送至客户端。
  • 若响应未完全发送(如数据过大),保留未发送部分;若发送完成,取消写事件注册(避免无数据时的空触发)。

# 3.3 事件优先级与优化策略

  • 读事件优先于写事件:
    客户端请求(读)需及时处理,否则可能导致客户端超时;而响应(写)可短暂延迟,因此事件循环中先处理读事件,再处理写事件。

  • 按需注册写事件:
    仅当输出缓冲区有数据需发送时,才向 epoll 注册写事件;发送完成后立即取消注册,避免套接字可写但无数据的“空轮询”(减少系统调用开销)。

# 四、时间事件:后台任务的调度机制

时间事件用于处理定时任务和周期性任务,是 Redis 维护服务器状态的核心机制,与文件事件共同构成事件循环的两大支柱。

# 4.1 时间事件的两种类型

  • 定时事件:仅执行一次,如 UNLINK 命令触发的大键异步删除(避免阻塞主线程)、临时任务调度。
  • 周期性事件:按固定间隔重复执行,核心是 serverCron 函数(Redis 最关键的后台任务入口)。

# 4.2 核心周期性任务:serverCron 函数

serverCron 由配置项 hz 控制执行频率(默认每秒 10 次,可通过 hz 配置调整,范围 1-500),主要负责以下工作:

  1. 服务器状态维护:
    更新当前时间、计算内存使用量、统计 CPU 利用率、更新 LRU 时钟(用于键淘汰)。

  2. 过期键清理:
    遍历过期字典,随机抽取部分键检查是否过期并删除(结合惰性删除:访问键时才检查过期),平衡内存占用与性能。

  3. 持久化管理:
    检查是否满足 RDB 快照触发条件(如 save 60 10000:60 秒内有 10000 次写操作)或 AOF 重写条件(如当前 AOF 文件大小超过上次重写后 100%)。

  4. 客户端管理:
    关闭长时间空闲(超过 timeout 配置)的客户端连接,释放资源;清理输出缓冲区溢出的客户端(避免内存泄漏)。

  5. 集群与哨兵同步:
    维护集群节点心跳、更新节点状态;哨兵模式下同步主从信息、检测主节点是否下线。

  6. 性能优化:
    执行内存碎片整理(activedefrag 配置开启时)、更新命令执行次数统计、重置峰值内存记录等。

# 4.3 时间事件的实现逻辑

Redis 用无序链表存储时间事件(而非堆等有序结构),事件循环中处理流程如下:

  1. 遍历链表查找最早到期事件:
    循环遍历时间事件链表,计算每个事件的到期时间(when)与当前时间的差值,找到最早到期的事件。

  2. 计算 IO 多路复用的阻塞时间:
    将最早到期事件的剩余时间作为 IO 多路复用(如 epoll_wait)的超时时间——若没有时间事件,超时时间为 -1(无限阻塞);若有,则阻塞至事件到期或文件事件就绪。

  3. 处理已到期的时间事件:
    处理完文件事件后,再次遍历链表,执行所有 when 小于当前时间的时间事件;若为周期性事件,更新其 when 为下次到期时间。

为什么用无序链表?
Redis 中时间事件数量极少(通常仅 serverCron 一个周期性事件),遍历开销可忽略(O(1) 复杂度),无需复杂的有序数据结构。

# 五、事件循环:Redis 主线程的运行逻辑

Redis 启动后,主线程进入 aeMain 事件循环,不断循环处理文件事件和时间事件,核心流程如下:

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 1. 执行 beforeSleep 回调(如 serverCron 预热)
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        // 2. 监听文件事件,阻塞时间为最早到期时间事件的剩余时间
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}
1
2
3
4
5
6
7
8
9
10

# 详细步骤解析

  1. 执行 beforeSleep 回调:
    事件循环阻塞前,调用 beforesleep 函数(默认绑定 beforeSleep 方法),执行如更新服务器统计信息、处理后台任务队列等操作。

  2. 监听并处理文件事件:
    调用 aeProcessEvents 函数,通过 IO 多路复用接口(如 epoll_wait)监听文件事件,阻塞时间由“最早到期时间事件的剩余时间”决定。当文件事件就绪时,按“读事件优先于写事件”的顺序触发回调函数处理。

  3. 处理时间事件:
    文件事件处理完成后,遍历时间事件链表,执行所有已到期的事件(定时事件执行后删除,周期性事件更新下次到期时间)。

  4. 循环或退出:
    若未触发停止标志(stop=0),重复步骤 1-3;若触发(如 SHUTDOWN 命令),退出事件循环,关闭服务器。

# 总结:Redis 高性能的设计哲学

Redis 以“单线程 + 事件驱动”为核心,通过三大机制实现高性能:

  1. IO 多路复用:借助 epoll 等技术,单线程高效监听数千个客户端连接,避免无效系统调用和空轮询。
  2. 双缓冲区协作:内核缓冲区与应用缓冲区分工明确,减少系统调用次数,平衡硬件速度差异。
  3. 事件驱动调度:Reactor 模式统一调度文件事件(网络请求)和时间事件(后台任务),保证单线程下的有序并发。

这种设计既规避了多线程的并发竞争问题,又突破了单线程的 IO 瓶颈,使 Redis 在高并发场景下仍能保持毫秒级响应,成为缓存、会话存储等场景的首选数据库。

编辑 (opens new window)
#Redis IO多路复用
上次更新: 2026/01/21, 19:29:14
Redis限流
Redis过期删除策略

← Redis限流 Redis过期删除策略→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式