Redis 线程模型(单线程/多线程)
候选人小孙在字节跳动的一面中,面试官问了一个"送命题":
"Redis 是单线程还是多线程?为什么?"
小孙秒答:"单线程,因为快。"面试官追问:"为什么单线程就快?"
小孙说:"...上下文切换少?"面试官:"那 nginx 也是单线程吗?"
小孙开始慌。
面试官继续追问:"Redis 6.0 引入了多线程,你知道吗?多线程用在什么地方?"
小孙彻底卡住,说:"好像是...网络 I/O?"
【面试官心理】
这道题我用来筛选"知其然不知其所以然"的候选人。知道 Redis 单线程的占 95%,能解释为什么单线程快的占 40%,能说出 6.0 多线程 I/O 细节的占 10%。Redis 的单线程模型是面试高频题,但大多数候选人只知其一不知其二。
一、Redis 为什么是单线程 🔴
1.1 问题拆解
第一层:Redis 单线程模型架构
graph TD
subgraph "Redis 6.0 单线程 vs 多线程"
A["客户端连接 1"]
B["客户端连接 2"]
C["客户端连接 N"]
D["Socket 文件描述符"]
E["I/O 多路复用 (epoll/select)"]
F["命令解析器"]
G["命令执行器"]
H["响应写入缓冲区"]
end
A --> D
B --> D
C --> D
D --> E
E --> F
F --> G
G --> H
Redis 6.0 之前,真正的"单线程"指的是命令执行是单线程,但网络 I/O 和命令解析实际上也是单线程在处理(严格来说是主线程)。
1.2 ❌ 错误示范
候选人原话:"Redis 单线程所以快,因为没有线程竞争。"
问题诊断:
- 把"单线程快"当成万能答案,没有深入分析
- 不理解 I/O 多路复用的原理
- 混淆了"单线程"和"无锁"的关系
面试官内心 OS:"这个候选人对 Redis 的理解还停留在表面,根本没有理解单线程模型的本质。"
1.3 标准回答
Redis 单线程的核心原因:
1. CPU 不是 Redis 的瓶颈:Redis 是内存型数据库,主要瓶颈在 I/O
2. 简化数据结构:无需为每个数据结构加锁
3. 避免上下文切换:无线程调度开销
4. 借助 I/O 多路复用:用 epoll 等机制处理高并发
Redis 真正的瓶颈是内存和网络,不是 CPU。
1.4 为什么单线程快?
关键在于 Redis 是 I/O 密集型 而非 CPU 密集型:
// Redis 单线程命令执行伪代码
while (true) {
// 1. 等待事件就绪 (I/O 多路复用)
aeApiPoll(eventLoop, timeout);
// 2. 处理就绪事件
for (每个就绪的 fd) {
// 3. 读取命令
readCommand(fd);
// 4. 解析命令
parseCommand();
// 5. 执行命令 (这里就是单线程的核心!)
executeCommand();
// 6. 写回响应
writeResponse(fd);
}
}
【面试官心理】
这道题我想验证的是候选人是否理解"单线程"的真正原因。99%的候选人知道 Redis 单线程,但只有 30%能解释为什么是单线程。能说出"I/O 多路复用"的占 40%,能解释 epoll 原理的占 10%。Redis 作者 antirez 的原话是:"Redis 是单线程的,因为 CPU 从来不是 Redis 的瓶颈。"
二、I/O 多路复用 🔴
2.1 为什么需要 I/O 多路复用?
传统的阻塞 I/O 模型:
// 阻塞 I/O:每个连接一个线程
while (true) {
int client = accept(listen_fd);
// 每个客户端都要开一个线程,线程开销巨大!
pthread_create(handle_client, client);
}
10万个连接 = 10万个线程 = 内存爆炸 + CPU 调度崩溃。
2.2 Redis 的 I/O 多路复用
// Redis 事件循环 (ae.c)
typedef struct aeEventLoop {
int maxfd; // 最大文件描述符
int setsize; // 监听的最大连接数
aeApiState *apidata; // 底层 I/O 多路复用的状态
aeFileEvent *events; // 注册的事件
aeTimeEvent *timeEvents; // 定时事件
} aeEventLoop;
Redis 支持多种 I/O 多路复用实现,根据平台自动选择:
// ae.c 自动选择最佳实现
#ifdef HAVE_EPOLL
api = &aeApiStatePoll; // Linux: epoll
#elif defined(HAVE_KQUEUE)
api = &aeKqueueState; // macOS: kqueue
#else
api = &aeSelectState; // fallback: select
#endif
2.3 epoll 的优势
// select 的问题:每次调用都要传入所有 fd,O(n) 遍历
fd_set readfds;
select(maxfd+1, &readfds, NULL, NULL, timeout);
// epoll 的优势:注册一次,回调通知,O(1) 复杂度
int epfd = epoll_create(1);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
struct epoll_event events[1024];
epoll_wait(epfd, events, 1024, timeout);
【面试官心理】
I/O 多路复用是 Redis 高性能的核心之一。我追问这个问题的深度在于让候选人理解:Redis 单线程不是"没办法",而是"故意的设计"。用 epoll 的 O(1) 事件通知 + 单线程无锁执行,这才是 Redis 快的原因。
三、Redis 6.0 多线程 I/O 🟡
3.1 追问
面试官追问:Redis 6.0 引入了多线程,用在哪些地方?
这是 2024 年面试的新热点。
Redis 6.0 的多线程只在网络 I/O 层面:
graph TD
subgraph "主线程"
A["命令解析"]
B["命令执行"]
C["结果写回"]
end
subgraph "I/O 线程"
D["读请求"]
E["写响应"]
end
D -->|分配任务| A
B -->|结果| E
注意:命令执行仍然是单线程的!
3.2 为什么要引入多线程 I/O?
瓶颈分析:
- 命令执行(单线程):10万 QPS,CPU 利用率 30%
- 网络 I/O(单线程):10万 QPS,CPU 利用率 70%
- 结论:网络 I/O 成了新的瓶颈!
多线程 I/O 的工作流程:
1. 主线程建立连接,分配 client
2. I/O 线程读取请求数据 → 主线程解析命令
3. 主线程执行命令
4. I/O 线程写回响应 → 主线程释放 client
3.3 ❌ 错误示范
候选人原话:"Redis 6.0 变成了多线程,执行命令也是多线程的。"
问题诊断:
- 完全混淆了多线程 I/O 和多线程执行
- 不知道 Redis 的命令执行为什么不能多线程
- 不理解多线程 I/O 的具体分工
面试官内心 OS:"这个候选人肯定没有仔细看过 Redis 6.0 的设计文档,只是在网上扫了一眼'多线程'三个字。"
3.4 标准回答
// redis.conf 配置
io-threads 4 // I/O 线程数(主线程 + 3 个工作线程)
io-threads-do-reads yes // 是否在 I/O 线程中执行读操作
💡
Redis 6.0 多线程默认是关闭的,需要手动开启。对于高并发短连接场景(如 Lua 脚本、HGETALL),多线程 I/O 可以提升 30%~50% 的吞吐量。但对于命令执行本身,单线程仍然是最优解。
【面试官心理】
这道题我想考察的是候选人对 Redis 版本演进的关注程度。Redis 6.0 是 2019 年发布的,但很多候选人到 2024 年都不知道这个变化。能说出多线程 I/O 的具体细节的占 10%,能解释为什么命令执行不能多线程的占 5%。命令执行不能多线程的原因是 Redis 的数据结构(Hash、ZSet 等)不是线程安全的,如果要并发执行,就要给每个操作加锁,反而更慢。
四、主线程 vs 多线程执行对比 🟡
4.1 为什么命令执行不能多线程?
Redis 的数据结构不是线程安全的:
// 如果两个线程同时操作同一个 Hash
// 线程 A: HGET + HSET
// 线程 B: HDEL
// → 数据错乱!
如果要用多线程执行命令,代价是给每个数据结构操作都加锁:
// 多线程 + 锁的方案
pthread_mutex_lock(&hash_lock);
HGET hash key;
pthread_mutex_unlock(&hash_lock);
// → 大量锁竞争,性能退化
Redis 作者的选择:单线程 + 无锁 = 最高性能。
4.2 什么操作是 CPU 密集型的?
SCAN、JGETALL、KEYS * → 命令解析
SMEMBERS → 序列化响应
KEYS * → 全量扫描
这些操作如果数据量大,即使单线程也可能占用大量 CPU。Redis 6.0 引入了 lazy free 来异步释放大对象:
lazyfreelazy_server_del yes
lazyfreelazy_expire yes
lazyfreelazy_user_del yes
【面试官心理】
我追问这个问题的深度在于验证候选人对"并发控制"的理解。能说出 Redis 命令执行为什么不能多线程的占 20%,能解释锁竞争问题的占 10%,能进一步提到 lazy free 的占 5%。
五、性能瓶颈分析 🟡
5.1 Redis 的性能天花板
graph TD
A["Redis QPS"] --> B{"瓶颈在哪里?"}
B -->|"内存足够"| C["网络 I/O"]
B -->|"单机网卡打满"| D["需要集群"]
B -->|"CPU 打满"| E["复杂命令太多"]
C --> F["多线程 I/O (6.0+)"]
D --> G["Redis Cluster"]
E --> H["优化命令 + pipeline"]
5.2 压测方法
# 简单 SET/GET 压测
redis-benchmark -t SET,GET -n 100000 -c 100
# 指定 key 大小
redis-benchmark -t SET -n 10000 -d 102400 # 100KB value
# Pipeline 压测
redis-benchmark -t SET -n 10000 -P 10 # 10 个 pipeline 请求
:::warning ⚠️
生产环境的坑:
- redis-benchmark 默认用 16 字节的 key 和 value,与生产数据差异巨大
- 压测单机和压测集群的瓶颈完全不同
redis-benchmark -c 100 只代表 100 个并发连接,实际生产可能是 1 万+
:::
六、生产避坑
# 查看 Redis 延迟原因
redis-cli --latency-history
# 查看命令耗时分布
redis-cli --latency-dist
# 查看慢查询
redis-cli SLOWLOG GET 10
【面试官心理】
这道题我想最终验证的是候选人的"全链路性能意识"。Redis 面试到最后,拼的不是谁背得多,而是谁理解得深。能把单线程模型、I/O 多路复用、6.0 多线程讲成一个完整逻辑链的,基本都是 P6+。