说结论
Redis 的"单线程"指的是:命令处理的主逻辑是单线程的。
但 Redis 进程里实际上有:
- 主线程:处理网络请求、执行命令、事件循环
- 3 个后台线程:异步处理关闭文件、AOF fsync、惰性释放
- 子进程:RDB 持久化、AOF 重写时 fork 出来的
所以 Redis 不是严格意义上的单线程,而是"命令处理单线程"。这个设计非常聪明,后面会解释为什么。
后台线程:bio.c
打开bio.c,文件开头的注释写得很清楚:
This file implements operations that we need to perform in the background. Currently there is a single operation, that is a background close(2) system call.
说"currently a single operation"是早期版本,现在已经扩展了。看bio.h的定义:
#define BIO_CLOSE_FILE 0 // 异步关闭文件 #define BIO_AOF_FSYNC 1 // 异步 AOF fsync #define BIO_LAZY_FREE 2 // 异步释放内存 #define BIO_NUM_OPS 3 // 共 3 种后台任务Redis 启动时会创建 3 个后台线程:
void bioInit(void) { // 初始化锁、条件变量、任务队列 for (j = 0; j < BIO_NUM_OPS; j++) { pthread_mutex_init(&bio_mutex[j],NULL); pthread_cond_init(&bio_newjob_cond[j],NULL); pthread_cond_init(&bio_step_cond[j],NULL); bio_jobs[j] = listCreate(); bio_pending[j] = 0; } // 创建 3 个线程 for (j = 0; j < BIO_NUM_OPS; j++) { if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) { serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs."); exit(1); } bio_threads[j] = thread; } }每个线程负责一种任务类型,有自己的任务队列。主线程通过bioCreateBackgroundJob提交任务:
void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) { struct bio_job *job = zmalloc(sizeof(*job)); job->time = time(NULL); job->arg1 = arg1; job->arg2 = arg2; job->arg3 = arg3; pthread_mutex_lock(&bio_mutex[type]); listAddNodeTail(bio_jobs[type],job); bio_pending[type]++; pthread_cond_signal(&bio_newjob_cond[type]); // 唤醒对应线程 pthread_mutex_unlock(&bio_mutex[type]); }后台线程的工作循环:
void *bioProcessBackgroundJobs(void *arg) { unsigned long type = (unsigned long) arg; while(1) { pthread_mutex_lock(&bio_mutex[type]); // 没任务就等着 if (listLength(bio_jobs[type]) == 0) { pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]); continue; } // 取任务 listNode *ln = listFirst(bio_jobs[type]); job = ln->value; pthread_mutex_unlock(&bio_mutex[type]); // 执行任务 if (type == BIO_CLOSE_FILE) { close((long)job->arg1); } else if (type == BIO_AOF_FSYNC) { redis_fsync((long)job->arg1); } else if (type == BIO_LAZY_FREE) { if (job->arg1) lazyfreeFreeObjectFromBioThread(job->arg1); else if (job->arg2 && job->arg3) lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3); } pthread_mutex_lock(&bio_mutex[type]); listDelNode(bio_jobs[type],ln); bio_pending[type]--; pthread_mutex_unlock(&bio_mutex[type]); } }典型的生产者-消费者模型。
为什么需要这些后台线程?
BIO_CLOSE_FILE:close()系统调用在某些情况下会阻塞。比如关闭一个大文件,或者 NFS 文件系统。主线程阻塞会导致所有客户端都卡住,所以放到后台线程做。
BIO_AOF_FSYNC:AOF 持久化需要定期fsync。这是个磁盘 IO 操作,可能很慢。appendfsync everysec配置就是每秒做一次 fsync,交给后台线程处理。
BIO_LAZY_FREE:UNLINK、FLUSHDB ASYNC、FLUSHALL ASYNC这些命令用到的。删除大 key(比如包含几百万元素的 hash)会阻塞主线程,所以放到后台线程慢慢删。这是 Redis 4.0 引入的特性。
子进程:持久化
RDB 快照和 AOF 重写会fork()子进程:
// rdb.c if ((childpid = fork()) == 0) { /* Child process */ closeListeningSockets(0); redisSetProcTitle("redis-rdb-bgsave"); // 执行持久化... exitFromChild(0); }// aof.c if ((childpid = fork()) == 0) { /* Child process */ closeListeningSockets(0); redisSetProcTitle("redis-aof-rewrite"); // 执行 AOF 重写... exitFromChild(0); }为什么用fork()而不是线程?因为 fork 出来的子进程有父进程内存的完整副本(写时复制),可以安全地遍历所有数据做持久化,不用担心主线程同时修改。如果是多线程,就要加各种锁,复杂度飙升。
但 fork 有代价:父进程内存越大,fork 越慢。所以 Redis 官方建议单实例内存不要太大。
主线程为什么是单线程的
回到核心问题:处理命令的主逻辑为什么用单线程?
几个原因:
1. 没锁的代价
多线程意味着共享数据要加锁。Redis 数据结构复杂,加锁会带来:
- 锁竞争开销
- 死锁风险
- 代码复杂度上升
单线程完全避免这些问题。
2. 瓶颈不在 CPU
Redis 大部分操作是内存操作,速度极快。瓶颈通常在:
- 网络带宽
- 客户端连接数
- 大 key 操作
多线程不一定能提升性能,反而增加复杂度。
3. 事件循环模型
Redis 用 epoll/kqueue 做多路复用,一个线程就能处理成千上万的并发连接。这种 IO 模型本身就是单线程友好的,Nginx 也是类似设计。
那些"慢"操作怎么办?
单线程最大的问题是:一个操作慢了,后面所有请求都得等。
Redis 的应对策略:
1. 把操作拆细
比如KEYS *会遍历所有 key,很慢。Redis 后来加了SCAN,每次只遍历一小部分,用游标续传。
2. 扔给后台线程
惰性删除(lazy free)就是这个思路。UNLINK命令异步删除大 key:
void unlinkCommand(client *c) { if (server.lazyfree_lazy_server_del) { // 异步删除 bioCreateBackgroundJob(BIO_LAZY_FREE, NULL, NULL, key); } else { // 同步删除(旧版本行为) dbDelete(c->db, key); } }3. 用子进程
持久化交给 fork 出来的子进程。
4. 直接禁止
KEYS命令在生产环境不建议用,DEBUG SLEEP也是调试用的。
那 Redis 6.0 的多线程 IO 是什么?
Redis 6.0 引入了多线程来处理网络 IO(读写 socket),但命令执行还是单线程。
这个特性的代码在networking.c里,主要解决的是网络带宽瓶颈问题。当客户端数据量很大时,读写 socket 成了瓶颈,可以用多个线程并行处理。
但核心的数据结构操作、命令执行,依然是单线程。
总结
| 线程/进程 | 职责 |
|---|---|
| 主线程 | 事件循环、命令执行 |
| bio 线程 1 | 异步关闭文件 |
| bio 线程 2 | 异步 AOF fsync |
| bio 线程 3 | 异步惰性释放 |
| 子进程 | RDB 持久化、AOF 重写 |
Redis 的"单线程"是指命令处理的主流程。但像文件关闭、fsync、大 key 删除这些可能阻塞的操作,都用后台线程或子进程处理了。
这是一个务实的设计选择。单线程简单、无锁、容易维护,配合异步 IO 和后台任务,足以应付绝大多数场景。