1. 先拆解核心问题
Redis 高性能模型经常被问成一句话:Redis 为什么这么快?是不是单线程?Redis 6 以后是不是多线程?
这个问题不能只回答“Redis 是单线程”。准确说法是:
| 问题 | 准确回答 |
|---|---|
| Redis 命令执行是不是单线程 | 核心命令执行路径仍然由主线程串行执行 |
| Redis 是否完全没有多线程 | 不是,后台任务、异步释放、网络 I/O 线程都可能用到额外线程 |
| Redis 6 的多线程做了什么 | 主要优化网络读写,不是把命令执行改成多线程并发 |
| Redis 为什么单线程还能快 | 内存操作、I/O 多路复用、数据结构设计、避免大量锁竞争 |
更严谨地说,Redis 不是“整个进程只有一个线程”的数据库,而是“命令执行主路径采用单线程模型”的内存数据库。
2. Redis 的单线程到底单在哪里
Redis 的“单线程”主要指网络事件到命令执行这一条主流程:读取请求、解析命令、执行命令、返回响应,在经典模型里由主线程完成。
这带来两个直接好处:
- 数据结构不需要在每次访问时加复杂锁。
- 命令按顺序执行,很多问题可以用原子命令或 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 准备好了,再处理哪个。

常见实现有 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 线程也参与读取请求 |
配置时要注意三点:
- Redis 默认不会因为升级到 6/7 就自动把命令执行变成多线程。
- 不要配置得比 CPU 核数还激进,线程调度也有成本。
- I/O 线程适合网络读写成为瓶颈的场景,不会修复慢命令和 BigKey。
- 线上改动前要压测,低并发场景打开多线程不一定收益明显。
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 和客户端侧指标。
8. 大对象释放为什么要用 UNLINK
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、内存、网络、持久化和客户端连接,而不是简单归因到单线程。