如何理解redis是单线程的

写在文章开头

在面试时我们经常会问到这样一道题

你刚刚说redis是单线程的,那你能不能告诉我它是如何基于单个线程完成指令接收与连接接入的?

这时候我们经常会得到沉默,所以对于这道题,笔者会直接通过3.0.0源码分析的角度来剖析一下redis单线程的设计与实现。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解redis的单线程模型

单线程处理核心任务

当我们通过./redis-server启动redis时,如果我们配置了后台启动,那么shell进程线程就会调用系统函数即fork方法创建一个子进程,再通过execve方法将子进程主体替换成redis可执行文件也就是我们的redis-server,而子进程执行时会保持从父进程集成过来的标准输入和输出,最后redis就会调用main方法开始执行自己的启动逻辑了。

在这里插入图片描述

到这为止,我们不难看出,在启动阶段redis的启动并不是多线程的,它会根据我们的配置来决定启动逻辑,以我们上文所说的后台启动,它本质是通过父进程fork的方式完成创建与初始化的,这一点我们也可以直接从redis的main方法印证:

int main(int argc, char **argv) {//命令参数解析与初始化//......//如果配置后台启动,则调用daemonize从父进程中fork出来执行if (server.daemonize) daemonize();//......
}

我们步入daemonize方法,可以看到其内部如果子进程fork成功,后续的标准输入、输出、错误都会重定向到/dev/null,由此后的各项工作也都是我们的redis server的主线程进行负责处理:

void daemonize(void) {int fd;//fork返回0说明fork成功,创建新会话,然后父进程exit(0)直接退出if (fork() != 0) exit(0); /* parent exits */setsid(); /* create a new session *//* Every output goes to /dev/null. If Redis is daemonized but* the 'logfile' is set to 'stdout' in the configuration file* it will not log at all. *///将标准输入、输出、错误重定向写到/dev/null中,由此和终端分离if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);if (fd > STDERR_FILENO) close(fd);}
}

后续的主线程的socket就会注册到epoll中,通过非阻塞调用epoll函数获取就绪的连接和指令完成与多个客户端的交互:

在这里插入图片描述

而上述所说这种工作模式,也就是我们的aeMain方法,这里笔者也给出的对应的的代码实现,如下所示,aeMain的本质逻辑就是调用无限循环,在循环中调用aeApiPollepoll非阻塞轮询获取就绪的事件并交给对应的读写事件处理器(rfileProc/wfileProc)进行处理:

//无限循环调用aeProcessEvents处理读写事件
void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);aeProcessEvents(eventLoop, AE_ALL_EVENTS);}
}int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{//......//通过epoll完成非阻塞调用numevents = aeApiPoll(eventLoop, tvp);//遍历拿到的事件将其交给读写处理器处理for (j = 0; j < numevents; j++) {//解析出该文件对应的类型aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];int mask = eventLoop->fired[j].mask;int fd = eventLoop->fired[j].fd;int rfired = 0;//如果事件fe是读事件则交给rfileProcif (fe->mask & mask & AE_READABLE) {rfired = 1;fe->rfileProc(eventLoop,fd,fe->clientData,mask);}//如果事件包含写标志,则交给wfileProc处理器处理if (fe->mask & mask & AE_WRITABLE) {if (!rfired || fe->wfileProc != fe->rfileProc)fe->wfileProc(eventLoop,fd,fe->clientData,mask);}processed++;}}//......//返回处理事件数return processed; /* return the number of processed file/time events */
}

多线程执行IO事件

截至到上述的片段,redis大体上我们可以认为是单线程执行,但是在3.0.0之后源码中,为了避免某些IO任务对主线程的执行效率的影响,redis还是创建了一些异步线程处理这些任务。
如下图所示,我们以aof为例,redis主线程会通过定时任务的方法serverCron会按照用户的配置检查当前是否需要进行aof写入,如果需要则通过bioCreateBackgroundJob提交一个任务到AOF异步刷盘的任务列表中,此时redis创建的io线程就会无限循环调用bioProcessBackgroundJobs从该列表中取出自己绑定的任务进行异步消费,通过这种简单的多线程模式,保证了耗时的IO操作不会阻塞主线程:

在这里插入图片描述

这里我们先给出对应的事件宏定义,可以看到事件总数为REDIS_BIO_NUM_OPS 即2,然后0是文件关闭事件,1的AOF异步刷盘事件,通过这样的顺序完成了事件的类型码和总量的定义:

/* Background job opcodes */
#define REDIS_BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
#define REDIS_BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
#define REDIS_BIO_NUM_OPS       2

对应的这些线程的初始化工作我们可以在main方法调用的initServer中可以看到这样一段调用,其内部的调用bioInit本质就是完成上述IO任务的线程的创建:

void initServer(void) {int j;//......//创建bio任务线程bioInit();
}

bioInit它会初始化2个线程以及栈大小(最大不会超过4M),为每个线程各自分配一个队列,分配队列这一步就会按照循环遍历得到的值进行分配,遍历时用REDIS_BIO_NUM_OPS作为范围控制,遍历到0的处理文件关闭事件,1则是AOF刷盘事件。
完成事件类型队列分配之后,redis会为每个线程分配消费任务的方法指针bioProcessBackgroundJobs,后续的线程的任务消费和处理都是调用这个方法执行的:

void bioInit(void) {pthread_attr_t attr;pthread_t thread;size_t stacksize;int j;//循环2次,刚刚好对应2个事件即0是文件关闭事件、1是aof刷盘事件for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {//互斥数组初始化pthread_mutex_init(&bio_mutex[j],NULL);//条件数组初始化pthread_cond_init(&bio_condvar[j],NULL);//bio任务数组初始化,每个数组元素都是一个任务列表bio_jobs[j] = listCreate();//表示每种任务列表待处理的任务数为0bio_pending[j] = 0;}//设置线程最大的栈属性大小,默认为1,若小于REDIS_THREAD_STACK_SIZE即4M则乘2pthread_attr_init(&attr);pthread_attr_getstacksize(&attr,&stacksize);if (!stacksize) stacksize = 1; while (stacksize < REDIS_THREAD_STACK_SIZE) stacksize *= 2;pthread_attr_setstacksize(&attr, stacksize);//创建线程并,为每一个线程分配一个任务列表for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {//循环两次 j为0即代表文件关闭事件、1是aof刷盘事件,这个arg会作为事件类型绑定到线程pthread上void *arg = (void*)(unsigned long) j;//调用pthread_create完成线程属性初始化和事件类型的绑定if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {redisLog(REDIS_WARNING,"Fatal: Can't initialize Background Jobs.");exit(1);}bio_threads[j] = thread;}
}

这里我们也给出bioProcessBackgroundJobs逻辑可以看到,每个线程调用该方法时,会在无限循环中根据任务的type按需消费处理:

void *bioProcessBackgroundJobs(void *arg) {struct bio_job *job;//每个线程都会根据自己传入的arg决定任务的type,0为文件关闭事件、1为aof刷盘事件unsigned long type = (unsigned long) arg;sigset_t sigset;//......//按照类型到bio_jobs取任务执行while(1) {listNode *ln;/* The loop always starts with the lock hold. */if (listLength(bio_jobs[type]) == 0) {pthread_cond_wait(&bio_condvar[type],&bio_mutex[type]);continue;}//取出自己需要处理的类型的队列任务ln = listFirst(bio_jobs[type]);job = ln->value;/* It is now possible to unlock the background system as we know have* a stand alone job structure to process.*/pthread_mutex_unlock(&bio_mutex[type]);//线程按照自己的类型进行消费if (type == REDIS_BIO_CLOSE_FILE) {close((long)job->arg1);} else if (type == REDIS_BIO_AOF_FSYNC) {aof_fsync((long)job->arg1);} else {redisPanic("Wrong job type in bioProcessBackgroundJobs().");}//完成后释放任务对象zfree(job);//线程解锁 任务移除pthread_mutex_lock(&bio_mutex[type]);listDelNode(bio_jobs[type],ln);bio_pending[type]--;}
}

了解的任务消费的源码之后,我们再来看看任务的投递的逻辑,我们以aof文件刷盘的任务为例,从定时任务函数serverCron,其内部会判断rdb或者aofpid不为-1,若不为-1则说明这两个其中一个重写任务完成了,直接内部逻辑,获取当前子进程的pid是否是aof子进程的,如果是则步入backgroundRewriteDoneHandler方法进行任务提交到任务队列中:

在这里插入图片描述

这里我们直接从serverCron为入口查看redis定时任务方法逻辑可以看到其内部会查看rdb_child_pid 或者aof_child_pid 是否为-1,判断是否有持久化任务完成了,若发现aof_child_pid 为-1且wait3获取到的pid也为aof的则调用backgroundRewriteDoneHandler提交异步刷盘任务:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {int j;REDIS_NOTUSED(eventLoop);REDIS_NOTUSED(id);REDIS_NOTUSED(clientData);//......//检查后台的aof重写进程是否结束,若结束的步入循环if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {int statloc;pid_t pid;//获取当前子进程pidif ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {//......if (pid == server.rdb_child_pid) {//......} else if (pid == server.aof_child_pid) {//如果pid 为aof_child_pid则调用backgroundRewriteDoneHandler提交任务到aof队列中backgroundRewriteDoneHandler(exitcode,bysignal);} else {redisLog(REDIS_WARNING,"Warning, detected child with unmatched pid: %ld",(long)pid);}updateDictResizePolicy();}} else {//......}//......
}

步入backgroundRewriteDoneHandler可以看到,如果AOF刷盘策略是AOF_FSYNC_EVERYSEC即异步刷盘则会调用aof_background_fsync进行文件刷盘,而该方法内部的逻辑就是调用我们上文的所说的提交后台任务方法bioCreateBackgroundJob:

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {//......if (server.aof_fd == -1) {/* AOF disabled, we don't need to set the AOF file descriptor* to this new file, so we can close it. */close(newfd);} else {/* AOF enabled, replace the old fd with the new one. */oldfd = server.aof_fd;server.aof_fd = newfd;if (server.aof_fsync == AOF_FSYNC_ALWAYS)aof_fsync(newfd);else if (server.aof_fsync == AOF_FSYNC_EVERYSEC)//如果是异步刷盘则将任务提交到对应的队列中aof_background_fsync(newfd);server.aof_selected_db = -1; /* Make sure SELECT is re-issued */aofUpdateCurrentSize();server.aof_rewrite_base_size = server.aof_current_size;/* Clear regular AOF buffer since its contents was just written to* the new AOF from the background rewrite buffer. */sdsfree(server.aof_buf);server.aof_buf = sdsempty();}server.aof_lastbgrewrite_status = REDIS_OK;//......} else if (!bysignal && exitcode != 0) {//......} else {//......}//......
}//调用bioCreateBackgroundJob提交任务到AOF刷盘队列中
void aof_background_fsync(int fd) {bioCreateBackgroundJob(REDIS_BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL);
}

小结

自此我们把redis中主线程和IO任务的线程都以图解和源码印证的方式分析完成了,以笔者的理解,设计者所说的redis是单线程的本质上的意思是说,对于核心的连接建立和指令处理是通过单个线程高效完成,而其余的一些非核心的IO耗时逻辑还是需要多线程来完成,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

参考

Linux内核学习笔记(4)-- wait、waitpid、wait3 和 wait4:https://www.cnblogs.com/tongye/p/9558320.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/34310.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[数据集][目标检测]花生米计数霉变检测数据集VOC+YOLO格式387张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;387 标注数量(xml文件个数)&#xff1a;387 标注数量(txt文件个数)&#xff1a;387 标注类别…

使用Leaflet和瓦片地图实现离线地图的技术指南

引言 在现代的Web应用中&#xff0c;地图服务扮演着越来越重要的角色。然而&#xff0c;在一些特殊环境下&#xff0c;如偏远地区或网络环境不稳定的情况下&#xff0c;依赖在线地图服务可能会受到限制。因此&#xff0c;实现离线地图功能成为了一个重要的需求。本文将介绍如何…

Redis入门与应用(1)

Redis的技术全景 Redis是一个开源的基于键值对&#xff08;Key-Value&#xff09;的NoSQL数据库&#xff0c;使用ANSI C语言编写&#xff0c;支持网络&#xff0c;基于内存但支持持久化。它性能优越&#xff0c;并提供多种语言的API。我们可以将Redis视为一个巨大的Map&#x…

秋招突击——第九弹——Redis缓存

文章目录 引言正文缓存基础旁路缓存模式&#xff08;重点&#xff09;读穿透&#xff08;了解&#xff09;写穿透&#xff08;了解&#xff09;异步缓存写入模式面试重点 缓存异常场景缓存穿透缓存击穿缓存雪崩面试重点 缓存一致性怎么保证&#xff1f;缓存一致性问题是什么方案…

[职场] 策略运营求职简历范文精选 #知识分享#微信#微信

策略运营求职简历范文精选 策略运营是用户运营的一种模式&#xff0c;主要针对于用户量级在千人到百万人规模的运营。下面是策略运营求职简历范文精选&#xff0c;供大家参考。 个人信息 姓名&#xff1a;蓝山 年龄&#xff1a;33岁 地址&#xff1a;北京 工作经验&#x…

C++STL梳理

CSTL标准手册&#xff1a; https://cplusplus.com/reference/stl/ https://cplusplus.com/reference/vector/vector/at/ 1、STL基础 1.1、STL基本组成(6大组件13个头文件) 通常认为&#xff0c;STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成&…

icloud 邮箱登入失败

APP NAME mail2HOSTING APP NAME cloudos2CLIENT TIME Tue Jun 11 2024 09:00:47 GMT0800 (中国标准时间) (1718067647802)USER AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36HOSTNAME www.icloud.…

使用阿里开源的Spring Cloud Alibaba AI开发第一个大模型应用

背景 前段时间看到Spring推出了SpringAI&#xff0c;可以方便快速的接入ChatGPT等国外的大模型&#xff0c;现在阿里巴巴也紧追脚步推出了Spring Cloud Alibaba AI&#xff0c;Spring Cloud Alibaba AI 目前基于 Spring AI 0.8.1 版本 API 完成通义系列大模型的接入。通义接入…

【Spine学习15】变换约束

变换约束&#xff1a;能让一个骨骼受另一个骨骼的变化影响。 1、选择m创建一个变换约束&#xff1a; 2、点击这个约束&#xff0c; 将移动数值拉的越满&#xff0c;m越接近s骨骼 当约束为0也就是默认的时候&#xff0c;m骨骼将不会受影响&#xff0c;变换约束可有可无。 tips…

Mysql简述

Java - sql语句学习 sql分类 sql语句 sql数据类型

《数字图像处理》实验报告一

一、实验任务与要求 1、用 matlab 编写空间域点处理操作处理给定的几幅图像&#xff0c;要求&#xff1a; 使用 imread 读取当前工作目录下的图像设计点处理操作并用代码实现处理用 imnshow 显示处理后的图像用 imwrite 保存处理后的图像 2、提交内容&#xff1a;m文件 实验…

ARM单片机使用CAN总线部署BootLoader

1.引言 1.1.单片机开发BootLoader意义 单片机开发BootLoader的原因主要与其在嵌入式系统中的关键作用有关。BootLoader是硬件启动的引导程序&#xff0c;它在操作系统内核或用户应用程序运行之前执行。以下是单片机开发BootLoader的主要原因&#xff1a; 初始化硬件设备&…

算法设计与分析:并查集法求图论桥问题

目录 一、实验目的 二、问题描述 三、实验要求 四、算法思想 1. 基准算法 1.1 算法思想 1.2 代码 1.3 时间复杂度 2. 使用并查集的高效算法 2.1 算法思想 2.2 代码&#xff1a; 2.3 时间复杂度&#xff1a; 五、实验结果 一、实验目的 1. 掌握图的连通性。 2. 掌…

卷积的通俗解释

以时间和空间两个维度分别理解卷积&#xff0c;先用文字来描述&#xff1a; 时间上&#xff0c;任何当前信号状态都是迄至当前所有信号状态的叠加&#xff1b;时间上&#xff0c;任何当前记忆状态都是迄至当前所有记忆状态的叠加&#xff1b;空间上&#xff0c;任何位置状态都…

python怎样自动提示

第一步、打开pycharm&#xff0c;如下图所示&#xff1a; 第二步、File→Power Save Mode&#xff0c;把下面如图所示的勾去掉&#xff1a; 第三步、去掉勾后&#xff0c;不再使用省电模式&#xff0c;新建一个python文件。输入单词前两个字母&#xff0c;就会自动提示了&#…

为什么说大模型训练很难?

前言 在人工智能的浪潮中&#xff0c;大模型训练无疑是一股不可忽视的力量。然而&#xff0c;这背后的过程却充满了挑战与困难。今天&#xff0c;让我们一同揭开大模型训练的神秘面纱&#xff0c;探讨为何它值得您的关注与投入。 大模型训练的挑战 大模型训练之所以难&…

选择门店收银系统要考虑哪些方面?美业系统Java源码分享私

开店前的一个重要事件就是选择门店收银软件/系统&#xff0c;尤其是针对美容、医美等美业门店&#xff0c;一个优秀专业的系统十分重要&#xff0c;它必须贴合门店的经营需求&#xff0c;提供更全面、便捷、高效的管理功能&#xff0c;帮助提升门店的服务质量和经营效益。 以下…

Python笔记 文件的读取操作

1.open()打开函数 再Python&#xff0c;使用open函数&#xff0c;可以打开一个已经存在的文件&#xff0c;或者创建一个新文件&#xff0c;语法如下 open(name,mode,encoding) name:是要打开的文件名的字符串&#xff08;可以包含文件所在的具体路径&#xff09; mode&…

【几何】多少正方形?

题目枚举边长为1边长为 2 \sqrt{2} 2 ​边长为 5 \sqrt{5} 5 ​边长为 8 \sqrt{8} 8 ​边长为 13 \sqrt{13} 13 ​ 扩展-使用代码来数1、定义点对象2、定义正方形对象3、初始化所有点4、调用完整代码 题目 多少正方形&#xff1f; 枚举 设每个横纵相邻点得间距为1&#xff0…

线程池概念、线程池的不同创建方式、线程池的拒绝策略

文章目录 &#x1f490;线程池概念以及什么是工厂模式&#x1f490;标准库中的线程池&#x1f490;什么是工厂模式&#xff1f;&#x1f490;ThreadPoolExecutor&#x1f490;模拟实现线程池 &#x1f490;线程池概念以及什么是工厂模式 线程的诞生是因为&#xff0c;频繁的创…