七、Redis之IO多路复用

📅 发布时间:2026/7/15 17:20:53 👁️ 浏览次数:
七、Redis之IO多路复用
Redis是单线程还是多线程这是一个经典的面试题答案不能简单说 “单线程” 或 “多线程”必须分版本和功能模块来回答。结论Redis 的核心命令执行是单线程的但从 6.0 版本开始引入了多线程来处理网络 IO且始终有后台多线程处理耗时任务。一、核心定论一句话回答Redis 是「核心单线程 辅助多线程」的混合架构。二、分版本看历史演变1、Redis 1.0 ~ 2.82009-2013纯单线程的 “初代版本”线程模型完全单线程网络 IOread/write、命令执行、过期键删除、AOF 刷盘全由 1 个主线程完成。依赖select/poll早期 IO 多路复用处理多连接但核心逻辑无任何多线程。核心背景Redis 定位是 “内存数据库”早期硬件单核 / 双核 CPU、网络带宽千兆网卡都有限单线程操作内存的速度纳秒级远快于网络和磁盘线程切换开销反而会拖慢速度。设计目标极简、高性能单线程天然避免锁竞争符合 “简单即高效” 的原则。存在的问题任何耗时操作比如删除大 Key、AOF 同步刷盘都会阻塞主线程导致 Redis 卡顿select/poll本身有性能上限文件描述符数量限制、轮询遍历高并发连接下效率下降。2、Redis 3.02015单线程核心不变新增集群能力线程模型核心逻辑仍单线程命令执行、网络 IO 还是 1 个主线程新增「集群管理辅助线程」仅用于处理集群节点间的心跳、槽位迁移等集群相关逻辑不参与命令执行和 IO 处理。核心改进和线程无关但影响重大)推出 Redis Cluster分布式集群方案支持数据分片、故障自动转移优化 IO 多路复用Linux 下默认改用 epoll替代 select/poll解决高并发连接的监听效率问题。IO多路复用优化3、Redis 4.02017首次引入 “后台多线程”缓解主线程阻塞线程模型核心逻辑单线程命令执行、网络 IO 仍由主线程完成新增「后台多线程异步任务」专门处理耗时操作核心包括1.异步删除线程执行 UNLINK 命令替代 DEL删除大 Key 时主线程仅标记 Key 为待删除后台线程异步释放内存2. AOF 刷盘线程把 AOF 缓冲区数据异步写入磁盘主线程只负责写缓冲区避免 fsync 阻塞3.关闭文件线程异步清理闲置的客户端连接文件描述符。关键区别划重点后台线程不参与任何命令执行仅处理 “脏活累活”后台线程数量固定默认 4 个用户无需配置Redis 内部管理。4、Redis 5.02018核心代码重构单线程模型优化线程模型完全继承 4.0 的 “核心单线程 后台多线程”无新增线程相关特性5、Redis 6.02020引入 IO 多线程突破网络 IO 瓶颈线程模型核心逻辑仍单线程命令执行、数据操作内存读写还是 1 个主线程新增「可配置 IO 多线程」专门处理网络 IOread/write和协议解析核心规则核心背景硬件升级万兆网卡普及单线程的 read()/write() 系统调用速度跟不上网卡传输速度多核 CPU 利用率低单线程仅用 1 核其余核闲置资源浪费。关键误区IO 多线程 ≠ 命令多线程执行命令执行仍单线程保证原子性IO 多线程是 “可选增强”中小并发场景QPS 50 万开启后收益有限甚至因线程切换略有损耗。实际影响多线程 IO并行处理网络读写充分利用多核 CPU吞吐量提升 30%-50%万兆网卡环境下Redis 吞吐量从 10 万 QPS 提升至 15 万 生产环境需根据 CPU 核心数和并发量合理配置 IO 线程数。6、Redis 7.02022 - 至今IO 多线程优化持续提升效率线程模型继承 6.0 的 “核心单线程 IO 多线程 后台多线程”优化 IO 多线程支持 “线程局部存储”“动态调整 IO 线程数”进一步降低线程切换开销新增「多线程哈希槽迁移」集群槽位迁移时用多线程并行处理提升迁移速度。形象比喻1. Redis 1.0~2.8纯单线程场景小饭馆只有 1 个老板主线程包揽所有活迎宾epoll 监听连接、点单read () 读命令、做饭执行 SET/GET 命令、送菜write () 写响应、刷盘子AOF 刷盘 / 删除大 Key问题只要刷盘子耗时操作慢了哪怕后厨有空新客人也得等老板刷完盘子才能点单 —— 对应 Redis 中 “耗时操作阻塞主线程导致所有请求排队”。2. Redis 3.0单线程核心 集群辅助线程场景饭馆生意火单门面坐不下老板开了分店Redis Cluster 集群老板主线程仍在总店负责核心的 “做饭 点单”雇了 1 个 “店长助理集群辅助线程”专门负责总店和分店之间的沟通心跳检测、客人分流槽位迁移不参与任何点单 / 做饭核心老板的核心工作做饭仍单线程只是新增辅助人员处理 “分店管理”解决 “容量不够” 的问题。3. Redis 4.0~5.0核心单线程 后台多线程场景老板发现 “刷盘子、清理餐桌脏活累活” 太耗时耽误点单做饭雇了4 个保洁阿姨后台线程专门负责刷盘子AOF 异步刷盘、清理餐桌UNLINK 删除大 Key、收拾闲置餐具关闭文件描述符老板主线程只专注 “点单 做饭”脏活累活全交给保洁阿姨哪怕阿姨刷盘子慢也不影响老板接新客人关键保洁阿姨绝不碰灶台不执行命令只做打杂的活且人数固定默认 4 个老板不用管阿姨们自己分工。4. Redis 6.0核心单线程 IO 多线程 后台多线程场景饭馆火到一次来 1 万个客人老板主线程一个人 “迎宾 点单网络 IO” 速度完全跟不上后厨做饭命令执行再快也没用随着硬件和网络宽带提升有了网络IO瓶颈。哪怕老板一次做一百万份但是点单一次只能点五千份老板雇了多个服务员IO 多线程专门负责 “迎宾监听后接客人 点单read () 读命令 送菜write () 写响应”老板主线程彻底专注 “做饭执行命令”服务员们并行点单、送菜充分利用饭馆的空间多核 CPU关键服务员只负责传菜 / 点单绝不做饭不执行命令且服务员数量可根据客人多少调整io-threads 配置客人少的时候甚至可以只雇 1 个默认关闭。5. Redis 7.0优化版 IO 多线程场景服务员之间配合更高效IO 多线程优化店长助理也学会了 “并行分流客人多线程槽位迁移”整体效率更高但老板仍只负责核心的 “做饭”。总结阶段线程模型核心解决的问题1.0-2.8纯单线程极简设计满足中小并发3.0核心单线程 集群辅助线程单节点性能触顶支持分布式集群4.0核心单线程 后台多线程耗时操作阻塞主线程5.0核心单线程 后台多线程代码重构优化单线程效率6.0核心单线程 IO 多线程 后台多线程网络 IO 瓶颈利用多核 CPU7.0核心单线程 优化版 IO 多线程进一步提升分布式 / 高并发场景效率Redis单线程时代但性能依l旧很快的主要原因根据以上我们得到核心结论redis是核心单线程IO多线程和后台多线程的架构。Redis早期为什么使用单线程使用单线程模型是 Redis 的开发和维护更简单因为单线程模型方便开发和调试即使使用单线程模型也并发的处理多客户端的请求主要使用的是IO多路复用和非阻塞IO对于Redis系统来说主要的性能瓶颈是内存或者网络带宽而并非 CPU。Redis为什么使用单线程依旧很快基于内存操作Redis 的所有数据都存在内存中。因此所有的运算都是内存级别的。所以他的性能比较高;数据结构简单Redis 的数据结构是专门设计的。而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1)。因此性能比较高多路复用和非阻塞 I/ORedis使用I/O多路复用功能来监听多个 socket连接客户端。这样就可以使用一个线程连接来处理多个请求。减少线程切换带来的开销。同时也避免了 I/O 阻塞操作避免上下文切换因为是单线程模型因此就避免了不必要的上下文切换和多线程竞争这就省去了多线程切换带来的时间和性能上的消耗。而且单线程不会导致死锁问题的发生Redis为什么又要加入多线程在Redis6/7中非常受关注的第一个新特性就是多线程。这是因为Redis一直被大家熟知的就是它的单线程架构虽然有些命令操作可以用后台线程或子进程执行比如数据删除、快照生成、AOF重写。但是从网络IO处理到实际的读写命令处理都是由单个线程完成的。随着网络硬件的性能提升Redis的性能瓶颈有时会出现在网络IO的处理上也就是说单个主线程处理网络请求的速度跟不上底层网络硬件的速度,为了应对这个问题:采用多个IO线程来处理网络请求提高网络请求处理的并行度Redis6/7就是采用的这种方法。但是Redis的多IO线程只是用来处理网络请求的对于读写操作命令Redis仍然使用单线程来处理。这是因为Redis处理请求时网络处理经常是瓶颈通过多个IO线程并行处理网络操作可以提升实例的整体处理性能。而继续使用单线程执行命令操作就不用为了保证Lua脚本、事务的原子性额外开发多线程互斥加锁机制了(不管加锁操作处理)这样一来Redis线程模型实现就简单了IO多路复用IO模型先讲最根本什么是 IOIO Input / Output。就是**数据从外部 → 进到程序里读 IO。数据从程序里 → 发到外部写 IO。**外部可以是**网络网卡、硬盘。键盘、鼠标**程序自己不能直接碰硬件必须通过操作系统内核。搞懂两个角色应用程序比如Redis、Nginx、MySql它想从网络/磁盘读数据、写数据操作系统内核OS只有它有权限碰网卡、硬盘。数据必须先到内核再交给应用程序。一次 IO 分为两个阶段必须记住任何一次网络 / 文件读取都一定分为 两个阶段等待数据到达等网卡来数据 → 放到内核缓冲区把数据从内核拷贝到应用程序内核 → 应用程序RedisIO 模型就是描述程序在这两个阶段里是等还是不等怎么等谁来通知不同的 “等待方式 通知方式”就是不同的 IO 模型。IO 模型 程序如 Redis和 操作系统内核 之间约定好的「数据读写协作方式」。它只决定三件事程序在等数据时会不会被卡住程序是主动去问还是内核来通知数据拷贝是程序自己做还是内核帮你做完Unix网络编程中的五中IO模型Blocking IO -阻塞I0应用程序调用 read()内核开始等待数据数据没来应用程序被卡住休眠啥也干不了等数据到了内核内核把数据复制到应用程序应用程序才恢复运行特点两个阶段 全程阻塞一个连接 一个线程并发高一点就崩一句话调用了就死等数据不来不返回。NoneBlocking IO - 非阻塞IO、轮询式 IO。应用程序调用 read()内核数据还没来立即返回错误不阻塞应用程序过一会再调用 read()内核还没来 → 再返回直到数据来了内核复制数据到程序返回成功特点等待阶段 不阻塞但程序要 不停轮询非常消耗 CPU一句话程序不停问 “好了吗”没好就去干别的反复问。IO multiplexing - IO多路复用。高并发服务器标配select /poll/epoll。程序不是等一个连接而是等一堆连接调用 select/poll/epoll内核监视所有 socket哪个有数据了就告诉程序程序再去对那个 socket 进行 read特点单线程就能处理大量连接真正高并发基石一句话一个人盯一堆桌子哪桌好了就处理哪桌。signal driven IO - 信号驱动IO程序先注册一个信号回调立刻返回去干别的数据到内核后内核发信号给程序程序收到信号再调用 read 读取数据特点等待数据不阻塞但复制数据时还是要阻塞实际中用得很少一句话数据到了内核内核发个通知程序再来读。asynchronous IO - 异步I0最完美、真正的异步。程序调用异步读接口立刻返回完全不管内核自己等待数据内核自己复制数据到程序全部做完后内核通知程序完成了特点真正完全异步实现复杂系统支持不一一句话发起请求就不管了内核全部做完再通知你。IO multiplexing - IO多路复用I/0网络I/0尤其在操作系统层面指数据在内核态和用户态之间的读写操作多路多个客户端连接连接就是套接字描述符即 socket 或者 channel)、复用复用一个或几个线程。I0多路复用也就是说一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接无需创建或者维护过多的进程/线程一句话一个服务端进程可以同时处理多个套接字描述符。linux世界一切皆是文件这句话的意思非常简单普通文件是文件目录是文件键盘是文件显示器是文件网络连接Socket也是文件只要是能读 / 写的东西Linux 都把它当成文件来管理。文件描述符、简称FD句柄就是一个数字一个非负整数0、1、2、3、4、5……它的作用只有一个让进程能找到自己打开的文件。内核里有一张表每个进程比如 Redis在内核里都有一张表进程 —— 内核文件记录表 —— 文件描述符FD可以把它理解成饭店的桌号。饭店 内核桌子 文件 / 网络连接桌号 文件描述符 FD你要吃饭必须先拿桌号。进程要读写文件 / 网络必须先拿 FD。FD是怎么来的流程非常简单1.进程说我要打开一个文件 / 建立一个网络连接 2.内核收到请求 3.内核在记录表中找一个空闲位置 4.把这个位置的编号返回给进程 5.这个编号就是 文件描述符以后进程想读写这个文件 / 连接只需要报这个编号内核就知道是哪一个。最关键网络连接也是 FD需要理解的是这句Socket 网络连接 文件 用文件描述符表示客户端 A 连 Redis → 生成一个 FD比如 3客户端 B 连 Redis → 生成一个 FD比如 4客户端 C 连 Redis → 生成一个 FD比如 5epoll 监听的就是一堆文件描述符 FD这就是为什么IO 多路复用 一个线程监听一堆 FD什么是epoll实现IO多路复用的模型有3种可以分select-poll-epoll三个阶段来描述。通过前面内容可以得知早期的redis用的select/poll实现的IO多路复用在3.xx的版本用epoll替代了select/poll区分select/poll/epoll模拟一个tcp服务器处理30个客户socket。假设你是一个监考老师让30个学生解答一道竞赛考题然后学生做完就举手我们负责验收学生答卷下面是区别selectpollepoll行为1.老师先规定“我最多只能盯 1024 个学生FD 上限超过的不管”2.学生举手后老师只知道 “有人举手了”但不知道具体是谁3.老师必须挨个走到学生面前问“是你举的手吗”轮询所有学生3.哪怕只有 1 个学生举手也得把 30 个学生全问一遍。1.老师取消了 “最多盯 1024 个学生” 的限制30 个都能盯2.学生举手后老师还是只知道 “有人举手”不知道具体是谁3.依然要挨个问 30 个学生“是你举的手吗”。1.老师不限制盯的学生数量30 个 / 3000 个都能盯2.学生举手后老师手里的小本本直接写着 “C、D、E 举手了”直接返回具体 FD3.老师直接走到 C、D、E 面前验收不用问其他人4.学生列表只在最开始交给老师一次FD 只拷贝一次后续不用重复给。技术对应1.select 有FD 数量上限默认 10242.只返回 “有事件”不返回具体 FD需轮询所有 Socket3.每次调用都要把 FD 列表从用户态拷贝到内核态效率低。1.poll 无 FD 数量上限2.但仍需轮询所有 Socket且 FD 列表仍需重复拷贝效率比 select 略好但核心问题没解决。1.epoll 无 FD 数量上限2.直接返回 “有事件的 FD 列表”无需轮询3.FD 列表只需拷贝一次mmap 共享内存效率最高4.事件驱动只通知 “变化的事件”无空轮询。epoll 的本质epoll 是Linux 内核提供的 IO 多路复用机制核心工具专门解决 “单个线程高效监听大量网络连接” 的问题 —— 它不是 Redis 的功能是操作系统给应用程序比如 Redis的 “监听工具”。redis主线程、IO线程、IO多路复用工作流程Redis 6.0 已开启 io-threads 22 个 IO 线程主线程负责核心命令执行IO 线程负责网络读写epoll 负责监听所有 Socket 事件。阶段 1客户端发起连接请求Redis 建立 Socket 连接客户端 A、B 同时执行 redis-cli -h 127.0.0.1 -p 6379发起 TCP 连接请求这个连接请求先到达服务器内核的网卡内核将其转发到 Redis 监听的 6379 端口Redis 主线程早已调用 epoll_ctl()把 6379 端口的 Socket监听 Socket注册到 epoll 中并指定监听「accept 事件」新连接事件Redis 主线程调用 epoll_wait() 进入非阻塞等待状态内核检测到 6379 端口有新连接请求后会把「accept 事件」写入 epoll 的就绪事件列表并唤醒 Redis 主线程Redis 主线程通过 epoll 获取到 “有新连接” 的通知执行 accept() 系统调用完成与客户端 A、B 的 Socket 连接建立生成两个新的 SocketSocket_A、Socket_B主线程把Socket_A、Socket_B 再次注册到 epoll 中指定监听「可读事件」客户端发出redis命令时触发此时连接建立完成客户端进入等待发命令状态。阶段 2客户端发送命令epoll 触发可读事件客户端 A 执行 SET A valA、客户端 B 执行 SET B valB命令以字节流形式通过网络发送到服务器字节流到达内核后被写入 Socket_A、Socket_B 的接收缓冲区内核检测到 Socket_A、Socket_B 的接收缓冲区有数据触发「可读事件」并把这两个事件写入 epoll 的就绪事件列表唤醒 Redis 主线程Redis 主线程调用 epoll_wait()从 epoll 中获取到 “就绪事件列表”[Socket_A可读, Socket_B可读]主线程把 Socket_A、Socket_B 放入「全局等待队列」并通过轮询策略分配Socket_A → 网络IO 线程 1Socket_B → 网络IO 线程 2主线程分配后继续处理其他任务比如清理过期键不阻塞。阶段 3网络IO 线程并行读取、解析命令IO 线程 1 对 Socket_A 执行 read() 系统调用从内核的接收缓冲区读取字节流*3\r\n$3\r\nSET\r\n$1\r\nA\r\n$4\r\nvalA\r\nIO 线程 1 将字节流解析为 Redis 可识别的命令对象{cmd: SET, key: A, value: valA}同理IO 线程 2 读取 Socket_B 的字节流并解析为命令对象{cmd: SET, key: B, value: valB} 两个 IO 线程并行完成解析后把命令对象提交到「主线程命令队列」。阶段 4主线程单线程执行命令主线程处理完手头的临时任务后从「命令队列」中按顺序取出命令对象先执行 SET A valA再执行 SET B valB主线程操作内存完成命令执行把 AvalA、BvalB 写入内存数据库生成响应结果OK针对 A、OK针对 B主线程把 “响应结果 对应 Socket” 放入「写任务队列」再次轮询分配Socket_AOK → IO 线程 1Socket_BOK → IO 线程 2。阶段 5IO 线程并行写回响应IO 线程 1 对 Socket_A 执行 write() 系统调用把响应 OK 写入 Socket_A 的发送缓冲区IO 线程 2 对 Socket_B 执行 write() 系统调用把响应 OK 写入 Socket_B 的发送缓冲区内核检测到发送缓冲区有数据将数据通过网卡发送回客户端 A、BIO 线程完成写操作后主线程把 Socket_A、Socket_B 重新注册到 epoll 中继续监听「可读事件」等待客户端后续命令。简化版流程连AB 发连接请求 → 内核接收 → Redis 主线程通过 epoll 监听到 accept 事件新连接事件 → 建 Socket 并注册到 epoll发AB 发 SET 命令 → 内核写接收缓冲区 → epoll 触发可读事件 → 主线程把 Socket 分给 IO 线程解析IO 线程并行读字节流、解析命令 → 交给主线程执行主线程单线程执行 SET 命令 → 生成 OK回写IO 线程并行写 OK 到发送缓冲区 → 内核发回客户端监Socket 重新注册到 epoll等待下一次事件。IO 多路复用epoll具体体现在这 3 个核心环节1. 监听「新连接请求」accept 事件场景客户端 A、B 发起 TCP 连接请求体现Redis 主线程把「监听 6379 端口的 Socket」注册到 epoll告诉内核“帮我盯着有新连接就喊我”内核发现有新连接请求后通过 epoll 通知 Redis“有新连接来了”Redis 主线程只需要调用 epoll_wait() 等待不用自己去轮询检查有没有新连接。这就是 IO 多路复用用 1 个线程同时监听「新连接事件」和「已建立连接的读写事件」。2. 监听「客户端发命令」可读事件场景客户端 A、B 发送 SET A / SET B 命令体现连接建立后Redis 把 Socket_A、Socket_B 也注册到 epoll告诉内核“帮我盯着这两个连接有数据就喊我”内核发现 Socket_A、Socket_B 有数据客户端发命令通过 epoll 通知 Redis“这两个连接可以读了”Redis 主线程拿到「就绪 Socket 列表」只有 A、B 两个只处理这两个其他空闲连接完全不管。这是 IO 多路复用 最核心的体现1 个线程同时监听成千上万个连接只处理有数据的连接避免无效轮询。3. 监听「响应写回」可写事件场景Redis 要把 OK 响应写回客户端体现当 Socket 的发送缓冲区满了写操作会阻塞Redis 会把这个 Socket 注册到 epoll监听「可写事件」内核发现发送缓冲区有空位了通过 epoll 通知 Redis“这个连接可以写了”Redis 再继续把剩余响应数据写回。这也是 IO 多路复用避免主线程阻塞在某个 Socket 的写操作上继续监听其他连接。一句话总结IO 多路复用贯穿全程IO 多路复用epoll就是 Redis 主线程的「眼睛」它帮主线程盯着所有连接新连接、已连接、读事件、写事件只有当某个连接真的有 “事情发生”有新连接、有数据可读、缓冲区可写才会通知主线程主线程不用挨个去问 “你有数据吗你能写吗”只处理 epoll 告诉它的「就绪连接」这就是多路复用的本质。IO多路复用和网络 IO 多线程的区别帮你彻底分清IO 多路复用epoll解决「怎么高效找到要处理的连接」—— 是 “筛选器”1 个线程就能管上万连接IO 多线程Redis 6.0解决「怎么高效处理这些连接的读写」—— 是 “搬运工”多个线程并行读 / 写数据解放主线程。下面是一个极简流程图把「epoll 监听 → 分配 IO 线程 → 主线程执行 → IO 线程写回」这几个环节标出来一眼就能看出 IO 多路复用在哪里。1. 客户端 A、B 发起连接 ↓ 2. 内核收到连接请求 ↓ 3. 【主线程 epoll】 ✅ 这里就是 **IO 多路复用** 主线程调用 epoll_wait()等内核通知 内核告诉主线程有新连接来了 ↓ 4. 主线程建立 Socket生成文件描述符 FD 把 Socket 注册到 epoll继续监听「可读事件」 ↓ ----------------------------------- ✅ 以上IO 多路复用负责「监听谁有数据」 ----------------------------------- 5. 客户端 A 发 SET A客户端 B 发 SET B ↓ 6. 内核收到数据通知 epoll epoll 告诉主线程 → FD_A 可读 → FD_B 可读 ↓ 7. 主线程把这两个 FD 放入全局队列 轮询分配给 **IO 线程 1、IO 线程 2** ↓ 8. 【IO 线程并行工作】 IO 线程 1 读 FD_A → 解析命令 IO 线程 2 读 FD_B → 解析命令 ↓ 9. 命令交给 **主线程** ----------------------------------- ✅ IO 线程只做读数据、解析、写响应 ----------------------------------- 10. 【主线程单线程执行】 先执行 SET A 再执行 SET B ↓ 11. 执行完毕生成响应 OK ↓ 12. 主线程把响应分发给 IO 线程 IO 线程并行写回给客户端 ↓ 13. 完成Socket 重新回到 epoll 监听IO多路复用也就是Redis为什么这么快的一个核心原因了。