Skip to content

01.2 - 单线程模型与 I/O 多路复用

定位: Redis 高性能的引擎 —— 事件循环如何用单线程处理万级并发连接 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 原理理解、对比(select/poll/epoll)、Redis 6.0 多线程改动


一、这是什么?为什么需要它?

是什么

Redis 的事件驱动模型:一个线程通过 I/O 多路复用(multiplexing)同时监听数万个 socket,只在 socket 可读/可写时处理它,其余时间挂起等待。

为什么需要?

没有 I/O 多路复用的方案:

方案 2:单线程轮询每个 socket(非阻塞 + 忙等)
  while (true) {
    for (每个 socket) {
      if (可读) handle(socket);
    }
  }

痛点:CPU 空转(没有任何请求时也在循环)

I/O 多路复用的方案(select/poll/epoll):

  while (true) {
    epoll_wait(fds);    ← 阻塞等待,内核告诉你"哪些 fd 好了"
    for (每个就绪的 fd) {
      handle(fd);       ← 只处理已就绪的
    }
  }

优势:无请求时停等、有请求时不漏不空转

二、原理拆解

2.1 I/O 多路复用三兄弟对比

维度selectpollepoll
监听方式轮询轮询回调通知
最大 fd 数1024无限制无限制
就绪通知遍历全部 fd遍历全部 fd只返回就绪的 fd
内核实现位数组扫描 O(n)链表扫描 O(n)红黑树+回调 O(1)
数据拷贝每次调用拷贝全量 fd同左通过 mmap 共享
平台所有 UnixPOSIXLinux 2.6+

为什么 epoll 胜出?

select:   每次调用都要把 10k 个 fd 从用户态拷贝到内核态 → 10k * 4K 数据拷贝
poll:     同上,只是用链表突破 1024 限制
epoll:    
  ① epoll_ctl 注册 fd — 一次拷贝
  ② 内核将就绪 fd 通过回调链入 ready list
  ③ epoll_wait 只返回就绪的 fd — 无需遍历全部

场景:10,000 并发连接,其中 100 个活跃
  select/poll:遍历 10000 个 fd,找到 100 个
  epoll:直接返回 100 个

2.2 Redis 事件循环完整流程

2.3 为什么单线程"够用"?

传统误区:
  瓶颈在 CPU → 单线程不够用 → 多线程更快

Redis 的真实情况:
  ✔ 缓存随机访问 ≈ 100ns(不是瓶颈)
  ✔ 命令执行时间 ≈ 1-10μs(纯内存)
  ✔ 网络读写 ≈ 10-100μs(真正瓶颈)

结论:CPU 不是瓶颈,网络 I/O 才是
      单线程 + epoll = 最优性价比

单线程的隐性优势:

单线程

  • 无锁——所有操作天然串行
  • 无上下文切换——CPU 时间 100% 做有用功
  • 无缓存失效——CPU cache 命中率高
  • 延迟可预测——不会因为锁竞争产生 jitter
  • 实现简单——没竞态条件的 bug

2.4 Redis 6.0 的多线程网络 I/O

6.0 之前的瓶颈:

  • ❌ 读命令:主线程 read(fd, buf) 拷贝数据
  • ❌ 写结果:主线程 write(fd, buf) 拷贝数据
  • ❌ 大 GET 响应(如 1MB 字符串)会阻塞主线程

6.0 的多线程优化:

  • ✅ 网络 I/O 用线程池(默认 3 个 reader + 3 个 writer)
  • ✅ 主线程仍然——解析命令、执行命令
  • ✅ 命令执行仍是单线程——数据结构操作不变

三、图解全景


四、实战验证

4.1 验证 epoll 事件处理

bash
# 启动 Redis,观察事件循环
redis-cli

# 在另一个终端查看进程
ps aux | grep redis-server

# strace 观察 epoll 调用(Linux 验证)
sudo strace -p $(pgrep redis-server) -e trace=epoll_wait,read,write -c

# 输出按 Ctrl+C 后:
# % time    秒  calls     syscall
# 50.00   0.5   1000    epoll_wait    ← 绝大部分时间在等待
# 30.00   0.3   500     read          ← 读取请求
# 20.00   0.2   400     write         ← 发送响应
# CPU 实际使用率通常 10-30%

4.2 验证单线程的瓶颈在大 key

bash
# 1. 创建一个 10MB 的大字符串
redis-cli -x SET bigkey < /dev/urandom  # 生成 10MB 随机数据

# 或用循环创建
redis-cli
127.0.0.1:6379> SET bigkey "$(dd if=/dev/zero bs=1M count=10 2>/dev/null | base64)"
OK

# 2. 测试大 key 对延迟的影响(在高并发下)
# 一个终端持续发送微请求
redis-cli --latency -h 127.0.0.1 -p 6379

# 另一个终端执行大 key 操作
redis-cli GET bigkey   # 会阻塞事件循环 10-100ms

# 观察 latency 输出:本来稳定的 <1ms 延迟出现跳变
# min: 0, max: 85, avg: 1.23  ← max 跳到了 85ms!

4.3 验证 Redis 6.0 多线程效果

bash
# 查看是否启用多线程
redis-cli
127.0.0.1:6379> CONFIG GET io-threads
1) "io-threads"
2) "4"                 # 4 个 IO 线程

# 基准测试对比
# 关闭多线程
redis-cli CONFIG SET io-threads 1
redis-benchmark -n 100000 -c 50 -d 1000 -t GET,SET

# 开启多线程(建议=CPU核心数)
redis-cli CONFIG SET io-threads 4
redis-benchmark -n 100000 -c 50 -d 1000 -t GET,SET

# 对大 Value(1KB+)场景提升明显

五、面试视角

追问答案要点
Redis 为什么是单线程的?瓶颈在内存/网络,不在 CPU;单线程避免锁/上下文切换/缓存行失效;6.0 后网络 I/O 可配置线程池,命令执行始终单线程
select/poll/epoll 的区别?select 1024 上限+O(n)遍历;poll 无上限+O(n);epoll 回调通知+O(1)+mmap 共享内存
Redis 事件循环中,什么时候会有延迟?大 key 操作(O(N) 命令)、fork 写时复制(RDB/AOF rewrite)、内存 swap、定期删除大批次过期 key
Redis 6.0 多线程改了什么?网络 I/O 多线程(read/write),命令解析和执行仍是单线程;通过无锁队列传递任务
单线程下如何提升 Redis 吞吐?Pipeline(批量请求避免 RTT 开销)、Lua 脚本(服务端原子执行)、合理切片避免大 key
如果 Redis 的 CPU 使用率接近 100%?通常是命令复杂度高或大量短连接,考虑拆分分片、Pipeline 减少连接数、检查慢查询

📚 相关链接

  • **Redis 设计哲学** — 为什么单线程是合理权衡
  • **RESP 协议** — 通信协议层面的优化
  • **性能诊断** — 延迟问题的排查方法
  • ← 返回 **Redis基础与架构索引**

Knowledge4J — Java 知识库