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 多路复用三兄弟对比
| 维度 | select | poll | epoll |
|---|---|---|---|
| 监听方式 | 轮询 | 轮询 | 回调通知 |
| 最大 fd 数 | 1024 | 无限制 | 无限制 |
| 就绪通知 | 遍历全部 fd | 遍历全部 fd | 只返回就绪的 fd |
| 内核实现 | 位数组扫描 O(n) | 链表扫描 O(n) | 红黑树+回调 O(1) |
| 数据拷贝 | 每次调用拷贝全量 fd | 同左 | 通过 mmap 共享 |
| 平台 | 所有 Unix | POSIX | Linux 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基础与架构索引**