Skip to content

Redis 6.0+ 多线程真相

提出问题

"Redis 是单线程的"——这道面试送分题在 Redis 6.0 发布后有了新答案。很多人在简历上写"熟悉 Redis",面试官问起多线程时却答得模棱两可:有人说"Redis 6.0 开始多线程了,所以更快",有人说"还是单线程,多线程没什么用"。这两种说法都有偏差。

问题的核心在于:Redis 6.0 引入的多线程到底用在哪?是不是真的打破了单线程瓶颈?命令执行还安全吗?如果多线程 IO 这么好,为什么默认还不开?生产上该怎么配?搞清楚这些,才算真正理解 Redis 的 IO 模型。

分析问题

多线程 IO 干了什么,没干什么

Redis 6.0 的多线程只负责网络 IO 的读写命令执行仍然是单线程。完整流程分三步:

  1. 主线程通过 epoll 接收客户端连接,将读事件分发给 IO 线程
  2. IO 线程并行读取数据、解析协议(Redis 协议解析占 CPU),将解析好的请求放入队列
  3. 主线程串行执行命令,执行结果再交给 IO 线程并行写回客户端

画成时序图:

Client1      Client2        IO-thread-1    IO-thread-2    Main-thread
  |            |                |              |              |
  |-- SET a 1 -|--------------->|              |              |
  |            |-- GET b ------>|              |              |
  |            |                |-- read+parse |              |
  |            |                |-------------->|              |
  |            |                |              |              |
  |            |                |              |-- read+parse |
  |            |                |              |-------------->|
  |            |                |              |              |-- execute SET
  |            |                |              |              |-- execute GET
  |            |                |              |              |-- send results
  |<--- OK ----|                |<--------------|--------------|
  |            |<--- "b-value" -|<--------------|              |
  |            |                |              |              |

关键约束:IO 线程在读写时必须持有 io_threads_mutex 的锁,主线程分发完任务后会等待所有 IO 线程完成,再继续执行命令。所以 IO 线程之间是并行读,但 IO 线程和主线程之间是串行协调的。

这段代码是 Redis 6.0 源码 networking.c 中的核心逻辑(简化版):

c
/* 主线程分发 read 任务 */
void handleClientsWithPendingReadsUsingThreads(void) {
    int processed = listLength(server.clients_pending_read);
    // 将待读客户端按 round-robin 分给 IO 线程
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        int target_id = item_id % io_threads_num;
        listAddNodeTail(io_threads_list[target_id], c);
        item_id++;
    }
    // 唤醒 IO 线程开始工作
    for (int j = 1; j < io_threads_num; j++) {
        io_threads_pending[j] = listLength(io_threads_list[j]);
        atomic_set(&io_threads_mutex[j], 0);
    }
    // 主线程也处理自己的一份
    listRewind(io_threads_list[0], &li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        readQueryFromClient(c->conn);
    }
    // 等待所有 IO 线程完成
    for (int j = 1; j < io_threads_num; j++) {
        while(atomic_get(&io_threads_mutex[j]) != 1) {
            // busy-wait 自旋等待
        }
    }
}

注意这里用的是自旋等待(busy-wait),不是条件变量。因为 IO 线程处理很快(微秒级),自旋比线程切换更划算。但如果 IO 线程数太多,自旋对 CPU 就是浪费。

什么时候收益大,什么时候几乎没收益

多线程 IO 的收益取决于网络 IO 耗时占命令总耗时的比例

收益大的场景(网络 IO 占比高):

场景典型耗时分布预期提升
大 Value 传输(GET 1MB String)网络传输 800μs + 内存读取 2μs2-3x
批量操作(MGET 100 key)协议解析 50μs + 内存读取 10μs1.5-2x
高并发连接(1 万+连接)epoll 分发 + 解析 100μs1.2-1.5x

收益小的场景(命令执行占主导):

场景典型耗时分布预期提升
小 Value 纯操作(SET a 1)网络传输 10μs + 执行 1μs~5%
重命令(ZINTERSTORE 10 个集合)网络传输 10μs + 执行 10ms无提升

实际压测数据(来自某电商 Redis 集群的 benchmark):

  • 机器配置:8 核 CPU,32GB 内存,DDR4 2400MHz,万兆网卡
  • 测试工具:redis-benchmark -t SET,GET -n 1000000 -c 50
  • 小 Value(100 bytes)结果:
    • io-threads=1(默认):QPS 85,300
    • io-threads=2:QPS 92,100(+8%)
    • io-threads=4:QPS 106,700(+25%)
    • io-threads=8:QPS 97,500(-8%,对比 4 线程)
  • 大 Value(50KB)结果:
    • io-threads=1:QPS 22,100
    • io-threads=4:QPS 58,300(+164%)

大 Value 场景下提升接近 3 倍,因为网络 IO 占了主要时间。但内存带宽才是真正的天花板——单机 Redis 跑满 DDR4 带宽(约 20-30GB/s)后,CPU 再快也没用。

配置和生产建议

Redis 6.0+ 多线程默认关闭,需要显式开启:

bash
# redis.conf
io-threads 4
io-threads-do-reads yes

踩坑记录:有次我线上开了 io-threads 16(16 核机器),结果 Redis 延迟从 1ms 飙到 20ms,CPU 全部耗在自旋等待上。后来发现 io-threads 超过 CPU 核心数 / 2 后,自旋等待的 CPU 开销 > 并行 IO 的收益。

io-threads 建议设为核心数的一半(4 核设 2,8 核设 4):

  • 4 核:io-threads 2
  • 8 核:io-threads 4
  • 16 核:io-threads 6~8(需实测,因为自旋等待的 CPU 消耗随线程数线性增长)

验证是否生效:

bash
redis-cli info | grep io_threads
# io_threads_active:1   # 1 表示多线程 IO 已激活
# io_threads_active:0   # 配置了但没开 io-threads-do-reads

io_threads_active:0 的情况:你虽然配了 io-threads 4,但没开 io-threads-do-reads yes——这是非常容易踩的坑。配置了线程数不代表多线程 IO 生效,必须同时开 do-reads

和后台线程(lazy-free)是两回事

很多人把 Redis 6.0 的两大改进搞混:

特性实现方式线程数量解决的问题配置项
多线程 IO网络读写异步化可配(默认 1)减少网络 IO 延迟,提升吞吐io-threads
异步删除(lazy-free)后台线程回收内存固定 3 个避免大 key 删除阻塞事件循环lazyfree-lazy-*

lazy-free 的配置:

bash
lazyfree-lazy-user-del yes    # DEL 自动异步(需 Redis 6.2+)
lazyfree-lazy-flush yes       # FLUSHALL/FLUSHDB 异步
lazyfree-lazy-eviction yes    # 淘汰时异步释放
lazyfree-lazy-expire yes      # 过期删除异步

典型场景:线上有个缓存 key 存了 200MB 的 JSON 数据,DEL 命令执行了 2.3 秒,期间所有其他请求排队等待。开 lazyfree-lazy-user-del yes 后,DEL 立即返回,后台线程慢慢回收内存。

这两个是完全不同的线程池——IO 线程处理网络,后台线程处理内存回收。命令执行仍然是单线程的,所以用 Redis 做分布式锁(SETNX)的原子语义没有变化。

6.2 和 7.0 的演进

  • Redis 6.0:引入多线程 IO,但只处理网络读写
  • Redis 6.2lazyfree-lazy-user-del 默认开启,io-threads-do-reads 默认改为 yes(但需配 io-threads > 1
  • Redis 7.0:引入 redis-cli --threads 支持多线程压测,服务端多线程 IO 无大变化,但优化了锁竞争(io_threads_mutex 替换为 atomic 操作)

总结

面试话术示例:"Redis 6.0 的多线程只针对网络 IO 读写,命令执行仍然是单线程。IO 线程默认关闭,需要手动配置 io-threads 并开启 io-threads-do-reads yes 才能生效。收益取决于网络 IO 占比,大 Value 和批量操作场景提升明显(实测 50KB 大 Value 提升 164%),小 Value 场景提升有限(约 10-25%)。建议 io-threads 设为核心数的一半,避免过多线程导致上下文切换和自旋等待抵消收益。这和 lazy-free 的后台线程是两回事,不要混淆。另外要注意,6.2 版本 io-threads-do-reads 默认就是 yes,但 6.0 不是,升级时要注意配置差异。"

关键点清单:

  • 多线程 IO 只做网络读写,不做命令执行,源码用自旋等待同步
  • 默认关闭(6.0),需 io-threads-do-reads yes 开启;6.2 开始默认 yes
  • 收益瓶颈在内存带宽,不是 CPU,设太多线程反而有害
  • 和 lazy-free 后台线程是两个独立线程池,不冲突
  • 分布式锁的原子语义不受影响
  • 生产建议:io-threads 设为核心数一半,压测验证后再上

参考:Redis 官方文档 IO Threads — https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/io-threads/

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。