当前位置: 首页 > news >正文

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

http://www.cnnetsun.cn/news/3099227.html

相关文章:

  • 【EI会议征稿进行中】第六届电子通信与计算机科学技术国际学术会议(ECCST 2026)
  • 光模块耦合,到底 “耦合” 了什么?
  • ESP芯片烧录终极指南:从零开始掌握esptool.py完整操作流程
  • 如何快速掌握Audacity:免费音频编辑的完整指南
  • OpenMP并行编程优化与性能调优实践
  • 如何高效使用抖音无水印下载工具:专业用户的完整方案指南
  • 真实用户见证:八位企业负责人的GEO实践访谈
  • 第二篇:系统功能测试实战:图书借阅模块 BUG 排查与修复代码
  • 美图ai模特一键换装,提升电商图片质感的实用工具全测评
  • 国内开发者开通 ChatGPT Plus 屡屡支付失败?记一次 ChatGPT Plus 国内订阅踩坑全过程:支付拦截底层原因拆解 稳定替代方案实操记录
  • AI编码助手真实提效20%-30%:聚焦样板代码、文档摘要与低风险重构
  • 外卖佣金涨到20%之后,我算了一笔账:为什么越来越多商家开始自己搞配送?
  • 计算机毕业设计之基于机器学习的个性化智能推荐系统的设计与实现
  • 方壳电池pack生产线如何选择?
  • SSH密钥实战指南:从原理到配置,实现安全免密登录与自动化运维
  • 小程序同城配送和上门收件发快递新功能发布
  • 空洞骑士模组管理器Scarab:5分钟搞定100+模组安装的终极指南
  • 终极Axure中文界面汉化指南:3分钟解锁流畅原型设计体验
  • IDEA ER图生成失败?7类典型报错代码级溯源+4种兼容性修复模板(含PostgreSQL 15/MySQL 8.4适配清单)
  • 【JetBrains官方未公开文档】:Inspect Code规则引擎底层原理与自定义检查器开发实录
  • 智慧职教刷课脚本:3分钟实现全平台自动学习
  • MWC26上海直击!移远割草机器人解决方案:让庭院作业“智”在必得
  • Adobe Illustrator智能脚本合集:终极设计自动化指南
  • 解放双手:taskt桌面自动化工具完整入门指南
  • SpringBoot+Vue图书管理系统环境搭建全过程 + 核心功能代码实现 + 踩坑复盘
  • 6.25-6.28 伟大可以被计划吗?-说会英语
  • 面对面 Java 面试:从视频直播到微服务的全景探讨
  • Bilibili Toolkit终极指南:如何高效管理你的B站账号与自动化操作
  • 必火GEO工具能解决什么,不能替代什么:企业使用前要看清边界
  • DXVK:跨越图形API鸿沟的翻译艺术