跳至主要內容

程序员小富大约 5 分钟

大家好,我是小富。

《十万个why》系列持续更新中

Redis 6.0 发布时最大的噱头就是"多线程"。很多人一听就兴奋了:Redis 终于上多线程了,性能要起飞了吧?

但当你真正了解之后会发现,Redis 6.0 的"多线程"跟你想象的完全不是一回事。它的核心命令执行依然是单线程的,多线程只用在了网络 IO 上。

一个号称"多线程"的版本,干活的还是单线程。这是什么操作?

先搞清楚 Redis 的性能瓶颈在哪

Redis 快,快到什么程度?单实例轻松跑到 10 万+ QPS。这么快的原因大家都知道:

  1. 数据在内存里,读写不走磁盘
  2. 单线程,没有锁竞争、没有上下文切换
  3. IO 多路复用(epoll),一个线程扛住上万连接

注意第 1 点和第 3 点。Redis 的操作都在内存里完成,一次 GET/SET 操作可能只需要几微秒。也就是说 CPU 从来不是 Redis 的瓶颈

真正的瓶颈在网络 IO。

一个完整的 Redis 请求流程是这样的:

客户端发送请求 → [网络传输] → Redis 读取数据(read) → 解析协议 → 执行命令 → 构造响应 → 写回数据(write) → [网络传输] → 客户端收到响应

其中「执行命令」只需要几微秒,但「读取数据」和「写回数据」涉及到系统调用(read/write),涉及到内核态和用户态的切换,在高并发场景下这部分开销远大于命令执行本身。

Redis 官方的测试数据显示:在高并发场景下,网络 IO 的读写占了整个请求处理时间的大部分。CPU 在等 IO,IO 在排队。

Redis 6.0 的多线程只做了一件事

Redis 6.0 引入多线程,不是让多个线程同时执行命令,而是让多个线程分担网络 IO 的读写工作:

之前(单线程全包):
主线程:读请求 → 解析 → 执行命令 → 构造响应 → 写响应 → 读请求 → ...

之后(多线程 IO):
IO 线程 1:读请求A、读请求B
IO 线程 2:读请求C、读请求D
IO 线程 3:读请求E、读请求F
         ↓ 解析完毕,放入队列
主线程:  执行命令A → 执行命令B → 执行命令C → ... (单线程顺序执行)
         ↓ 执行完毕,结果放入队列
IO 线程 1:写响应A、写响应B
IO 线程 2:写响应C、写响应D  
IO 线程 3:写响应E、写响应F

简单说就是:IO 多线程读写,命令单线程执行。

为什么命令执行不上多线程?

这是最核心的设计决策。Redis 选择命令执行保持单线程,不是技术上做不到,而是不值得。

原因一:单线程是 Redis 的核心优势

Redis 的很多数据结构操作不是原子操作。比如一个 LPUSH + LTRIM 的组合操作,如果多线程并发执行,你要么加锁要么用 CAS,两者都会带来额外开销。

单线程意味着:

  • 所有命令天然串行,不需要任何锁
  • 不存在并发安全问题
  • 代码实现简单,bug 少
  • 每个命令的执行时间极短(微秒级),串行执行根本不是瓶颈

如果上了多线程执行,Redis 内部的所有数据结构都需要重新设计为线程安全的。String、List、Hash、Set、Sorted Set,每一个数据结构的每一个操作都要加锁或者用无锁并发结构。这不是加几个 mutex 就能搞定的,这是推倒重来。

原因二:命令执行不是瓶颈

一个 GET 命令在内存里就是一次哈希表查找,几微秒搞定。10 万 QPS 意味着每秒 10 万次哈希查找,对于现代 CPU 来说轻轻松松。

而 10 万 QPS 意味着每秒要处理 10 万次 read 系统调用和 10 万次 write 系统调用,每次系统调用都涉及内核态切换。这才是真正吃时间的地方。

所以 Redis 的决策是:在瓶颈上(IO)使用多线程,在非瓶颈上(命令执行)保持单线程。

原因三:和 Memcached 的不同选择

对比 Memcached,它是真正的多线程模型——多个 worker 线程各自处理完整的请求(包括命令执行)。但 Memcached 的数据结构简单(只有 KV),加锁的成本可控。

Redis 的数据结构比 Memcached 丰富得多(5 种基础类型 + Streams + Modules),让这些复杂数据结构支持并发访问的改造成本远高于 Memcached。

多线程 IO 的效果怎么样?

Redis 官方的测试数据:开启多线程 IO 后,在特定场景下吞吐量提升了约 2 倍。

# redis.conf 中开启多线程 IO
io-threads 4              # IO 线程数(建议设为 CPU 核数的一半或 2-3)
io-threads-do-reads yes   # 读也用多线程(默认只有写用多线程)

注意,多线程 IO 默认是关闭的。Redis 官方认为对于大多数场景,单线程 IO 已经够用了。只有在以下场景才建议开启:

  • QPS 超过 10 万
  • 大量大 value 的读写(比如缓存大 JSON)
  • CPU 核数充足

一个容易搞混的点:Redis 一直有"多线程"

很多人以为 Redis 6.0 之前是纯粹的单线程,这也不准确。Redis 在 4.0 就引入了后台线程来处理一些耗时操作:

  • BIO 线程:后台执行 close(fd)fsynclazyfree(异步删除大 key)等操作
  • UNLINK 命令(Redis 4.0):大 key 的删除放到后台线程执行,避免阻塞主线程

所以 Redis 的"单线程"一直指的是命令执行是单线程的,后台一直有辅助线程在干脏活累活。

总结

版本线程模型
Redis 4.0 以前命令执行 + IO 都是单线程,后台有 BIO 辅助线程
Redis 4.0新增 UNLINK、异步删除等后台线程
Redis 6.0网络 IO 多线程读写 + 命令执行单线程

Redis 6.0 的多线程本质是:在不动摇单线程命令执行这个核心设计的前提下,用多线程加速了 IO 这个真正的瓶颈。

这不是一个妥协,而是一个精准的工程决策:在该用多线程的地方用多线程,在不该用的地方坚守单线程。比"全部多线程化"要高明得多。


我是小富,下期见。

上次编辑于: