Redis 系列 8. 高性能模型:单线程、多线程与 I/O 多路复用


1. 先拆解核心问题

Redis 高性能模型经常被问成一句话:Redis 为什么这么快?是不是单线程?Redis 6 以后是不是多线程?

这个问题不能只回答“Redis 是单线程”。准确说法是:

问题 准确回答
Redis 命令执行是不是单线程 核心命令执行路径仍然由主线程串行执行
Redis 是否完全没有多线程 不是,后台任务、异步释放、网络 I/O 线程都可能用到额外线程
Redis 6 的多线程做了什么 主要优化网络读写,不是把命令执行改成多线程并发
Redis 为什么单线程还能快 内存操作、I/O 多路复用、数据结构设计、避免大量锁竞争

更严谨地说,Redis 不是“整个进程只有一个线程”的数据库,而是“命令执行主路径采用单线程模型”的内存数据库。

2. Redis 的单线程到底单在哪里

Redis 的“单线程”主要指网络事件到命令执行这一条主流程:读取请求、解析命令、执行命令、返回响应,在经典模型里由主线程完成。

这带来两个直接好处:

  1. 数据结构不需要在每次访问时加复杂锁。
  2. 命令按顺序执行,很多问题可以用原子命令或 Lua 脚本解决。

这里说的“避免锁竞争”只针对主线程命令执行模型。Redis 进程里仍然可能存在后台线程、子进程和异步任务,只是它们不把普通命令执行改成多线程并发修改同一份数据。

但这不代表 Redis 只有一个执行单元。比如:

能力 说明
RDB 后台保存 bgsave 会 fork 子进程,不是主线程同步写完整 RDB
AOF 重写 bgrewriteaof 也会使用后台子进程
异步释放 UNLINK、lazy free 可以把大对象释放放到后台线程
I/O 线程 Redis 6 起可以配置网络 I/O 多线程

这里容易说错:Redis 6/7 的 I/O 线程不是让多条写命令同时修改同一份数据。命令执行仍然保持主线程串行,I/O 线程主要分担网络读写相关开销。

3. 为什么单线程还能扛住高并发

Redis 的快不是单点原因,而是几件事叠加:

原因 说明
数据在内存 大多数命令不走磁盘随机 I/O
数据结构紧凑 SDS、listpack、quicklist、skiplist 等结构针对场景优化
I/O 多路复用 单线程可以同时管理大量客户端连接
命令执行简单 大量命令复杂度是 O(1) 或 O(logN)
避免锁竞争 主流程串行,少了多线程共享数据结构的锁成本
系统调用更可控 事件循环集中处理就绪事件,减少无效阻塞和线程切换

如果请求都是简单 GET、SET、INCR、HGET,性能瓶颈通常先出现在网络、客户端连接数、慢命令、BigKey、持久化抖动,而不是“主线程不够用”这一句话上。

实际生产里还要把客户端行为放进来一起看。例如序列化成本、pipeline 批次大小、连接池配置、网络 RTT 都可能比 Redis 命令本身更早成为瓶颈。

4. I/O 多路复用解决了什么

如果一个连接一个线程,连接数上来以后线程切换成本会非常高。Redis 采用 I/O 多路复用,核心思路是:一个线程通过操作系统提供的机制同时监听多个 socket,哪个 socket 准备好了,再处理哪个。

Redis 文件事件处理器

常见实现有 select、poll、epoll:

实现 特点
select 有文件描述符数量限制,需要重复拷贝和遍历
poll 没有固定的 fd_set 限制,但仍然需要遍历
epoll Linux 上常用,事件就绪通知更高效,适合大量连接

Redis 在不同系统上会选择不同的事件模型。生产环境一般部署在 Linux 上,也就通常会用到 epoll。

从设计模式看,Redis 网络层可以理解为 single-threaded reactor pattern:事件循环等待 socket 可读、可写或定时事件,然后把事件分发给对应处理器。Redis 源码里的 aeEventLoop 就是事件循环的核心结构。

epoll 又有 level-triggered 和 edge-triggered 两类触发方式。Redis 主线更接近 level-triggered 语义:只要 fd 仍然就绪,事件循环后续还能继续收到通知。这种模型更容易写对,不需要应用层一次性把 socket 数据全部读空才能避免漏事件。

5. 五种 I/O 模型放到 Redis 里理解

Unix 网络编程里常说五种 I/O 模型:

模型 关键点 Redis 关系
阻塞 I/O 线程等待数据准备和数据拷贝 不适合一个线程管理大量连接
非阻塞 I/O 调用立即返回,需要不断轮询 单独使用会产生大量空轮询
I/O 多路复用 一个阻塞点监听多个 fd Redis 网络模型核心
信号驱动 I/O 通过信号通知 Redis 主线不靠这个模型
异步 I/O 内核完成后通知应用 Redis 网络主模型不是 AIO

所以 Redis 的重点不是“不会阻塞”,而是“主线程把阻塞点放在多路复用器上,等真正有事件时再处理”。

Redis 主线网络模型不依赖 signal-driven I/O,也不是基于内核 AIO 完成命令处理。它的核心仍然是事件驱动的 I/O 多路复用。

6. Redis 6/7 的 I/O 线程怎么配

Redis 配置项里有两个关键参数:

io-threads 4
io-threads-do-reads yes

说明如下:

配置 说明
io-threads I/O 线程数量,1 等价于不启用额外 I/O 线程
io-threads-do-reads 是否让 I/O 线程也参与读取请求

配置时要注意三点:

  1. Redis 默认不会因为升级到 6/7 就自动把命令执行变成多线程。
  2. 不要配置得比 CPU 核数还激进,线程调度也有成本。
  3. I/O 线程适合网络读写成为瓶颈的场景,不会修复慢命令和 BigKey。
  4. 线上改动前要压测,低并发场景打开多线程不一定收益明显。

io-threads 1 等价于不启用额外 I/O 线程。只有显式配置大于 1,并结合 io-threads-do-reads yes 时,读写两侧的网络 I/O 才可能被分担。即便如此,命令解析后的执行阶段仍然回到主线程。

配置数量通常要结合 CPU 核数和负载模型压测。常见做法是从较保守的线程数开始,例如不超过可用 CPU 核数,并观察吞吐、P99/P999 延迟和 CPU 上下文切换,而不是直接把线程数配满。

Redis 官方配置文件里也提醒过,I/O 多线程通常在请求量很大、网络 I/O 开销明显时才更有意义。

7. 慢命令比“单线程”更危险

主线程串行执行意味着一个慢命令会拖住后面的请求。下面这些命令或场景需要重点关注:

类型 风险
KEYS * 遍历全库,数据量大时阻塞主线程
HGETALL 大 Hash 返回和序列化成本都高
LRANGE 0 -1 大 List 一次拉全量会阻塞
SMEMBERS 大 Set 大集合全量返回风险高
删除 BigKey 同步释放内存可能造成明显卡顿
Lua 脚本过重 Lua 执行期间 Redis 不能处理其他命令

慢命令的本质是 head-of-line blocking:前面的命令占住主线程,后面的请求即使很简单,也只能排队等待。BigKey 和大结果集还会放大网络发送、客户端反序列化和 tail latency,常见表现是平均延迟不高,但 P99 / P999 抖动明显。

在 Cluster 模式下,KEYS * 的风险更难控制。它只扫描当前节点,但如果运维脚本对每个 master 都执行一遍,就会变成全节点扫描,影响范围更大。生产环境应优先使用 SCAN 分批处理。

Lua 脚本同样要控制执行时间。Redis 提供 SCRIPT KILL,但只能终止尚未执行写命令的脚本;如果脚本已经修改了数据,就不能安全 kill,只能等待执行结束或采用更强的运维手段。

可以先打开慢日志:

slowlog-log-slower-than 10000
slowlog-max-len 128

查看慢日志:

redis-cli slowlog get 10

这里的单位是微秒,10000 表示超过 10ms 记录。生产环境阈值要结合业务延迟目标设置。

慢日志保存在 Redis 内存结构中,不是持久化审计日志。实例重启、日志长度达到 slowlog-max-len 后都会丢失旧记录,因此生产排查通常还要配合监控系统、latency monitor 和客户端侧指标。

DEL 会删除 key,并释放对象内存。如果对象很大,释放过程也会消耗 CPU,并阻塞主线程。

Redis 4 引入 UNLINK,语义上也是删除 key,但内存释放可以交给后台线程异步完成:

unlink big:list
unlink big:hash

相关 lazy free 配置可以按场景打开:

lazyfree-lazy-user-del yes
lazyfree-lazy-expire yes
lazyfree-lazy-eviction yes

lazyfree 使用的是后台释放线程,和 Redis 6 的 I/O threads 不是同一类能力。前者处理对象内存释放,后者处理网络读写,两者都不能让慢命令本身变快。

注意:UNLINK 不是用来解决所有性能问题的。真正应该做的是避免业务层制造 BigKey,删除只是兜底动作。

9. 面试回答版本

如果面试里问 Redis 为什么快,我会这样回答:

Redis 的命令执行主路径是单线程串行的,不是整个 Redis 进程只有一个线程。这个模型避免了共享数据结构上的复杂锁竞争,也让命令原子性更容易保证。

Redis 之所以还能处理高并发,是因为它把内存数据结构、事件驱动网络模型、I/O 多路复用、低成本命令实现和客户端批量访问能力组合在一起。它的高性能不是“单线程”单独带来的,而是 architecture、data structure 和 OS model 共同作用的结果。

Redis 6 以后支持 I/O 多线程,但主要优化网络读写,命令执行仍然由主线程串行完成。所以打开 I/O 线程不能解决慢命令、BigKey、Lua 脚本过重这类问题。线上排查性能问题时,我会先看慢日志、BigKey、内存、网络、持久化和客户端连接,而不是简单归因到单线程。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录