问题
Redis 7 的线程模型是怎样的?为什么说Redis是单线程但又有多线程?
答案
1. 核心概念
Redis的线程模型经常被误解为”纯单线程”,实际上Redis采用的是“单线程命令执行 + 多线程IO处理” 的混合架构。
准确描述:
- 命令执行:单线程(保证原子性和简单性)
- 网络IO:多线程(提升吞吐量)
- 后台任务:多线程(避免阻塞)
2. 线程模型演进
2.1 版本演进历程
Redis 1.0-3.x (纯单线程)
|
+--> 主线程:网络IO + 命令执行 + 持久化
|
缺点:大key删除、AOF刷盘会阻塞
Redis 4.0 (引入BIO线程)
|
+--> 主线程:网络IO + 命令执行
+--> BIO线程:文件关闭、AOF刷盘、懒惰删除
|
改进:耗时操作异步化
Redis 6.0 (多线程IO)
|
+--> 主线程:命令执行
+--> IO线程:网络读写、命令解析
+--> BIO线程:后台任务
|
改进:网络IO并行化
Redis 7.0 (增强多线程)
|
+--> 主线程:命令执行(单线程)
+--> IO线程:网络读写(默认启用)
+--> BIO线程:后台任务(优化调度)
|
改进:性能优化、默认启用多线程
3. 完整线程架构
3.1 线程分类
Redis 7 线程架构
├── 主线程 (Main Thread)
│ └── 命令执行、事件循环
│
├── IO线程池 (IO Threads)
│ ├── IO Thread 1: 网络读写
│ ├── IO Thread 2: 网络读写
│ ├── IO Thread 3: 网络读写
│ └── IO Thread N: 网络读写
│
└── BIO线程池 (Background IO Threads)
├── BIO_CLOSE_FILE: 关闭文件
├── BIO_AOF_FSYNC: AOF刷盘
└── BIO_LAZY_FREE: 懒惰删除
3.2 线程协作流程
客户端请求完整流程:
[客户端] --1--> [IO线程] --2--> [主线程] --3--> [IO线程] --4--> [客户端]
↓ ↓ ↓
读取socket 执行命令 序列化响应
解析协议 (单线程) 写入socket
后台任务流程:
[主线程] --5--> [BIO线程]
↓
异步执行
(关闭文件/刷盘/释放内存)
4. 单线程命令执行
4.1 为什么命令执行是单线程?
优势:
- 无锁设计:避免多线程竞争和锁开销
- 原子性保证:命令天然原子执行
- 简单可靠:无并发bug,易于维护
- 高效执行:纯内存操作,单线程足够快
4.2 事件循环模型
// Redis主线程事件循环(简化版)
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
// 1. 处理定时事件(过期key清理、统计等)
processTimeEvents(eventLoop);
// 2. 等待文件事件(客户端连接、命令请求)
aeApiPoll(eventLoop, tvp);
// 3. 处理文件事件
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
// 读事件:接收客户端命令
if (fe->mask & AE_READABLE) {
fe->rfileProc(eventLoop, fd, fe->clientData, mask);
}
// 写事件:返回响应给客户端
if (fe->mask & AE_WRITABLE) {
fe->wfileProc(eventLoop, fd, fe->clientData, mask);
}
}
}
}
4.3 单线程性能瓶颈
# 单线程性能极限测试
redis-benchmark -t set,get -n 1000000 -q
# 单核CPU极限:约10万QPS
# 瓶颈:
# 1. 网络IO(socket读写)
# 2. 协议解析(RESP协议)
# 3. 响应序列化
5. 多线程IO优化
5.1 IO线程工作原理
// IO线程处理流程(简化版)
void *IOThreadMain(void *myid) {
long id = (long)myid;
while(1) {
// 等待主线程分配任务
listIter li;
listNode *ln;
listRewind(io_threads_list[id], &li);
// 处理读操作(解析命令)
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
readQueryFromClient(c); // 读取并解析命令
}
// 处理写操作(序列化响应)
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
writeToClient(c); // 序列化并写入socket
}
// 通知主线程完成
io_threads_pending[id] = 0;
}
}
5.2 主线程与IO线程同步
时间轴:主线程与IO线程协作
主线程 IO线程1 IO线程2
| | |
|-- 分配客户端 ---------->| |
| | |
|-- 分配客户端 -----------------------> |
| | |
|-- 等待完成 | |
| | |
| [读取+解析] [读取+解析]
| | |
|<-- 完成通知 ------------| |
|<-- 完成通知 -------------------------| |
| | |
|-- 执行命令(单线程) | |
| | |
|-- 分配响应 ------------>| |
|-- 分配响应 -----------------------> |
| | |
| [序列化+写入] [序列化+写入]
| | |
|<-- 完成通知 ------------| |
|<-- 完成通知 -------------------------| |
5.3 配置与性能
# redis.conf 配置
# IO线程数(默认4,建议值:CPU核心数的50%-75%)
io-threads 4
# 启用读多线程(默认yes)
io-threads-do-reads yes
# 性能提升对比:
# io-threads 1(单线程):10万QPS
# io-threads 4(多线程):25万QPS(提升150%)
# io-threads 8(多线程):35万QPS(提升250%)
6. BIO后台线程
6.1 三类BIO线程
// BIO线程类型定义
#define BIO_CLOSE_FILE 0 // 关闭文件
#define BIO_AOF_FSYNC 1 // AOF刷盘
#define BIO_LAZY_FREE 2 // 懒惰删除
// 任务队列
list *bio_jobs[BIO_NUM_OPS];
pthread_mutex_t bio_mutex[BIO_NUM_OPS];
pthread_cond_t bio_newjob_cond[BIO_NUM_OPS];
6.2 任务调度
// 主线程提交BIO任务
void bioCreateLazyFreeJob(lazy_free_fn free_fn, void *arg) {
struct bio_job *job = zmalloc(sizeof(*job));
job->free_fn = free_fn;
job->arg = arg;
pthread_mutex_lock(&bio_mutex[BIO_LAZY_FREE]);
listAddNodeTail(bio_jobs[BIO_LAZY_FREE], job);
pthread_cond_signal(&bio_newjob_cond[BIO_LAZY_FREE]);
pthread_mutex_unlock(&bio_mutex[BIO_LAZY_FREE]);
}
// BIO线程执行任务
void *bioProcessBackgroundJobs(void *arg) {
while(1) {
pthread_mutex_lock(&bio_mutex[type]);
// 等待任务
while(listLength(bio_jobs[type]) == 0) {
pthread_cond_wait(&bio_newjob_cond[type], &bio_mutex[type]);
}
// 取出任务
ln = listFirst(bio_jobs[type]);
job = ln->value;
pthread_mutex_unlock(&bio_mutex[type]);
// 执行任务
job->free_fn(job->arg);
}
}
7. 并发安全保障
7.1 数据结构无锁设计
// Redis核心数据结构(单线程访问,无需加锁)
typedef struct redisDb {
dict *dict; // 键空间(仅主线程访问)
dict *expires; // 过期字典(仅主线程访问)
dict *blocking_keys; // 阻塞键(仅主线程访问)
} redisDb;
7.2 IO线程同步机制
// IO线程计数器(原子操作)
_Atomic unsigned long io_threads_pending[IO_THREADS_MAX_NUM];
// 主线程等待IO线程完成
void waitIOThreadsFinish(void) {
for (int j = 1; j < server.io_threads_num; j++) {
while(io_threads_pending[j] != 0) {
// 自旋等待
}
}
}
8. 性能对比测试
8.1 不同线程配置性能
# 测试命令
redis-benchmark -t set,get -n 1000000 -c 100 -d 256 -q
# 结果对比:
# io-threads 1: SET: 95000 req/s, GET: 100000 req/s
# io-threads 2: SET: 160000 req/s, GET: 180000 req/s
# io-threads 4: SET: 250000 req/s, GET: 280000 req/s
# io-threads 8: SET: 320000 req/s, GET: 360000 req/s
8.2 监控指标
# 查看线程统计
INFO stats
# io_threaded_reads_processed: IO线程处理的读操作
# io_threaded_writes_processed: IO线程处理的写操作
# 查看CPU使用
INFO cpu
# used_cpu_sys: 系统CPU时间
# used_cpu_user: 用户CPU时间
9. 最佳实践
9.1 线程数配置建议
# 根据CPU核心数配置
# 4核CPU:io-threads 2-3
# 8核CPU:io-threads 4-6
# 16核CPU:io-threads 6-8
# 示例配置(8核CPU)
io-threads 4
io-threads-do-reads yes
9.2 Java客户端连接池配置
// Lettuce客户端配置(适配Redis多线程)
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
// 连接池配置
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(200); // 最大连接数
poolConfig.setMaxIdle(50); // 最大空闲连接
poolConfig.setMinIdle(20); // 最小空闲连接
poolConfig.setMaxWaitMillis(3000); // 最大等待时间
// 客户端配置
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(2))
.build();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration();
serverConfig.setHostName("localhost");
serverConfig.setPort(6379);
return new LettuceConnectionFactory(serverConfig, clientConfig);
}
}
9.3 性能调优检查清单
# 1. 检查Redis版本(建议7.0+)
redis-server --version
# 2. 检查多线程配置
CONFIG GET io-threads*
# 3. 检查CPU使用率(建议 < 80%)
INFO cpu
# 4. 检查慢查询(避免阻塞主线程)
SLOWLOG GET 10
# 5. 检查大key(避免阻塞)
redis-cli --bigkeys
10. 常见误区
10.1 误区1:Redis是纯单线程
错误:Redis只有一个线程 正确:命令执行是单线程,但有多个IO线程和BIO线程
10.2 误区2:多线程会导致并发问题
错误:多线程会有数据竞争 正确:命令执行仍是单线程,数据结构无并发访问
10.3 误区3:线程越多越好
错误:io-threads设为16或更多 正确:过多线程导致上下文切换开销,建议不超过CPU核心数
答题总结
Redis 7的线程模型是“单线程执行 + 多线程IO” 的混合架构:
- 主线程:单线程执行命令,保证原子性和简单性
- IO线程:多线程处理网络读写,提升吞吐量2-3倍
- BIO线程:后台处理耗时任务,避免阻塞主线程
- 并发安全:命令执行单线程,数据结构无锁设计
- 配置建议:io-threads设为CPU核心数的50%-75%
面试时可补充:Redis选择单线程执行命令是因为纯内存操作足够快,瓶颈在网络IO,因此通过多线程优化IO而非命令执行,这是一种精准的性能优化策略。