news 2026/7/2 7:51:13

edis 单线程真的是单线程吗?源码角度全面解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
edis 单线程真的是单线程吗?源码角度全面解析

说结论

Redis 的"单线程"指的是:命令处理的主逻辑是单线程的

但 Redis 进程里实际上有:

  1. 主线程:处理网络请求、执行命令、事件循环
  2. 3 个后台线程:异步处理关闭文件、AOF fsync、惰性释放
  3. 子进程: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_FILEclose()系统调用在某些情况下会阻塞。比如关闭一个大文件,或者 NFS 文件系统。主线程阻塞会导致所有客户端都卡住,所以放到后台线程做。

BIO_AOF_FSYNC:AOF 持久化需要定期fsync。这是个磁盘 IO 操作,可能很慢。appendfsync everysec配置就是每秒做一次 fsync,交给后台线程处理。

BIO_LAZY_FREEUNLINKFLUSHDB ASYNCFLUSHALL 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 和后台任务,足以应付绝大多数场景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 7:51:07

【EI会议征稿进行中】第六届电子通信与计算机科学技术国际学术会议(ECCST 2026)

2026年第六届电子通信与计算机科学技术国际学术会议将于2026年12月11-13日在中国西安举行。征稿主题计算机科学▪ 机器学习▪ 操作系统▪ 并行计算▪ 计算机编程▪ 演化计算▪ 信息可视化▪ 云计算与边缘计算▪ 量子计算▪ 算法与计算复杂性▪ 深度学习与神经网络电路与系统▪ …

作者头像 李华
网站建设 2026/7/2 7:50:51

光模块耦合,到底 “耦合” 了什么?

在 AI 算力集群、大型数据中心架构里&#xff0c;光模块承担电信号、光信号互相转换的核心职能。简单来讲&#xff0c;光模块一端对接电芯片&#xff0c;一端连接光纤&#xff1b;电信号要转为光信号&#xff0c;才能依托光纤远距离传输&#xff1b;抵达终端后&#xff0c;光信…

作者头像 李华
网站建设 2026/7/2 7:48:03

ESP芯片烧录终极指南:从零开始掌握esptool.py完整操作流程

ESP芯片烧录终极指南&#xff1a;从零开始掌握esptool.py完整操作流程 【免费下载链接】esptool Serial utility for flashing, provisioning, and interacting with Espressif SoCs 项目地址: https://gitcode.com/gh_mirrors/es/esptool esptool.py是乐鑫科技官方提供…

作者头像 李华
网站建设 2026/7/2 7:46:41

如何快速掌握Audacity:免费音频编辑的完整指南

如何快速掌握Audacity&#xff1a;免费音频编辑的完整指南 【免费下载链接】audacity Audio Editor 项目地址: https://gitcode.com/GitHub_Trending/au/audacity 还在为寻找一款专业又免费的音频编辑软件而烦恼吗&#xff1f;Audacity正是你需要的开源音频编辑器&…

作者头像 李华
网站建设 2026/7/2 7:46:38

OpenMP并行编程优化与性能调优实践

1. 并行编程优化概述并行编程是现代高性能计算的核心技术之一&#xff0c;它通过将计算任务分配到多个处理单元来提升程序性能。其基本原理包括任务分解、数据分布和同步机制等关键技术。在工程实践中&#xff0c;合理的并行化策略可以显著提升计算密集型应用的性能&#xff0c…

作者头像 李华
网站建设 2026/7/2 7:44:19

如何高效使用抖音无水印下载工具:专业用户的完整方案指南

如何高效使用抖音无水印下载工具&#xff1a;专业用户的完整方案指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback su…

作者头像 李华