Redis 为什么这么快?它的线程模型是怎样的?

进阶高频原理约 6 分钟读完

一句话回答

主要原因有四个:数据都在内存里;数据结构针对速度和内存做了专门设计;用 I/O 多路复用让一个线程同时处理大量连接;命令执行是单线程的,没有锁竞争和线程切换的开销。Redis 6.0 引入的多线程只用来读写网络数据和解析协议,命令仍由主线程逐个执行,单条命令依然是原子的。单线程的代价是一个慢命令会阻塞所有请求,所以生产环境要用 SCAN 代替 KEYS,避免对大 Key 做全量操作。

详细解析

快在哪里

原因 说明
内存存储 读写不经过磁盘,访问内存比访问磁盘快几个数量级;持久化在后台进行
高效的数据结构 字符串记录了长度,取长度是 O(1);哈希表 O(1) 查找;有序集合用跳表做到 O(log n);小数据用紧凑编码节省内存,见 数据类型和底层实现
I/O 多路复用 一个线程通过 epoll(Linux)、kqueue(macOS)同时监听成千上万个连接,哪个连接有数据就处理哪个,不会因为某个连接没数据而干等
单线程执行命令 数据结构不用加锁,没有线程切换和锁竞争;Redis 的瓶颈通常在内存和网络,而不是 CPU
简单的协议 RESP 协议解析简单;客户端还可以用 Pipeline 一次发送多条命令,减少网络往返

线程模型

Redis 6.0 之前,网络读写、协议解析和命令执行都在一个主线程里完成,结构是一个事件循环(和 Node.js 的事件循环 思路类似):

文本
主线程事件循环:
  等待事件(epoll_wait)
    ├─ 有新连接:accept
    ├─ 连接可读:读数据 → 解析命令 → 执行命令 → 结果写入输出缓冲区
    ├─ 连接可写:把输出缓冲区的数据发给客户端
    └─ 定时任务:清理过期 key 等

"单线程"指的是执行命令的只有一个线程,Redis 进程里还有其他线程和子进程:

  • 后台线程:关闭文件、AOF 的 fsync、异步释放内存(UNLINK、FLUSHALL ASYNC)
  • 子进程:生成 RDB 快照、重写 AOF 时 fork 出来,见 RDB 和 AOF

Redis 6.0 的多线程

并发连接很多时,瓶颈会出现在网络读写的系统调用和协议解析上。6.0 把这部分交给了 I/O 线程:

文本
主线程:收集可读的连接,分给各个 I/O 线程
I/O 线程(并行):读数据、解析命令
主线程:等所有 I/O 线程解析完,按顺序逐个执行命令
I/O 线程(并行):把结果写回客户端

命令执行仍是单线程的,所以数据结构不用加锁,原有的原子性保证不变。这个功能默认关闭,需要配置 io-threads(比如设为 4)开启。6.x、7.x 里开启后默认只有写回客户端走多线程,读和解析也要走多线程,还需要打开 io-threads-do-reads;Redis 8.0 重写了 I/O 线程的实现,读和写总是都交给 I/O 线程,这个配置不再起作用。

单线程的代价

一条命令执行 100 毫秒,这 100 毫秒里所有客户端的请求都在排队。常见的慢操作:

  • KEYS *:遍历所有 key,key 多时会阻塞很久
  • 对大 Key 做全量操作:HGETALL、SMEMBERS、LRANGE key 0 -1,以及用 DEL 删除一个很大的集合,见 大 Key 和热 Key
  • 执行时间很长的 Lua 脚本
  • 同步执行的 FLUSHALL、FLUSHDB

对策:用 SCAN 代替 KEYS,用 HSCAN、SSCAN、ZSCAN 分批遍历集合,用 UNLINK 代替 DEL 删除大 Key;用 SLOWLOG GET 查看慢命令(默认记录执行超过 10 毫秒的命令);生产环境可以用 ACL 或 rename-command 禁用 KEYS 这类危险命令。

代码示例

用 SCAN 分批遍历匹配的 key(ioredis):

JavaScript
async function scanKeys(redis, pattern) {
  const keys = new Set() // SCAN 可能返回重复的 key,用 Set 去重
  let cursor = '0'
  do {
    // 每次只遍历一小部分,命令很快返回,不会长时间阻塞其他请求
    const [next, batch] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 200)
    batch.forEach((k) => keys.add(k))
    cursor = next
  } while (cursor !== '0')
  return [...keys]
}

面试官可能追问

单线程怎么利用多核 CPU?

单个实例执行命令主要只用一个核。想用满多核,可以在一台机器上部署多个实例,或者用 Cluster 把数据分片到多个节点;6.0 之后的 I/O 线程、后台线程和持久化子进程也会用到其他核。

6.0 引入多线程后,还要担心命令的并发安全吗?

不用。多线程只负责读写网络数据和解析协议,命令仍然在主线程里排队逐个执行,单条命令、MULTI 事务和 Lua 脚本的原子性都和以前一样。需要注意的还是业务层面的竞态,比如先 GET 再 SET 是两条命令,中间可能插入别的客户端的命令。

SCAN 有哪些需要注意的地方?

SCAN 用游标分批遍历,返回的游标为 0 才算结束。遍历期间一直存在的 key 一定会被返回,但可能返回不止一次;遍历期间新增或删除的 key 可能返回也可能不返回。COUNT 只是每次遍历工作量的提示,不保证返回的条数;MATCH 是取出数据后才过滤的,所以某一次可能返回空列表但游标不为 0,这时要继续遍历。

Pipeline 为什么能提升性能?

不用 Pipeline 时,每条命令都要等上一条的响应回来才发送,大部分时间花在网络往返上。Pipeline 把多条命令一次发出去、一次读回结果,省掉了多次往返,也减少了读写的系统调用。但 Pipeline 不是原子的,中间可能穿插其他客户端的命令;一批也不宜太大,否则 Redis 要在内存里暂存大量的回复。

易错点

  • Redis 的"单线程"指命令执行是单线程,进程本身有多个线程,还会 fork 子进程
  • 6.0 的多线程默认是关闭的,而且只用于网络 I/O,不会并行执行命令
  • Redis 快的前提是命令本身快,O(N) 的命令在数据量大时同样会拖垮整个实例

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。