来聊聊redis文件事件驱动的设计

写在文章开头

近期团队安排变得比较紧急,关于redis系列的更新相对放缓一些,而我们今天要讨论的就是redis中关于事件模型的设计,我们都知道redis通过单线程实现高效的网络IO处理,本文会从源码的角度来讲解一下redis中文件事件驱动这一块的设计。

在这里插入图片描述

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

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

在这里插入图片描述

详解文件事件的设计与实现

单线程reactor模式的设计

Linux系统的思想是一切皆文件,所以在对于网络通信中服务端socket也是为其分配一个文件描述符(fd)并将其以文件的形式进行管理,并将其读写交给epoll进行轮询处理,由此构成一个单线程的reactor模型。

如下图所示,redis服务端的主线程会在每一次循环时通过epoll并将服务端socketfd传入非阻塞获取该文件就绪的事件,然后根据事件的类型有序的分发给对应的处理器进行处理。

在这里插入图片描述

对应的我们也给出redis文件事件循环框架的核心代码,可以看到其入参为server.maxclients+REDIS_EVENTLOOP_FDSET_INCR也就是10000+32+96,这就是事件循环框架每次可容纳的socket的文件描述符的大小:

	//创建事件循环框架server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);

对此我们步入aeCreateEventLoop即可看到事件循环框架的核心实现,其本质就是完成事件循环框架的初始化,通过上一步传入的大小创建创建socket的数组空间,记录每一个接入的客户端socket的事件。

aeEventLoop *aeCreateEventLoop(int setsize) {aeEventLoop *eventLoop;int i;//空间分配if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;//记录事件数组的大小eventLoop->setsize = setsize;//......//创建事件循环框架if (aeApiCreate(eventLoop) == -1) goto err;//基于传入的setsize初始化每一个文件描述符的events空间为AE_NONE,表示当前socket没有就绪的事件for (i = 0; i < setsize; i++)eventLoop->events[i].mask = AE_NONE;return eventLoop;//......}

后续的事件与轮询处理都会在aeMain中进行不断的轮询并转交分发器进行处理,这里我们也给出对应的核心代码,可以看到该循环本质就是传入轮询框架eventLoop,轮询所的事件AE_ALL_EVENTS

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;//循环while (!eventLoop->stop) {if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);aeProcessEvents(eventLoop, AE_ALL_EVENTS);}
}

我们步入其内部就可以看到对于服务端套接字(socket)的事件非阻塞轮询查看和事件分发处理逻辑:

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{int processed = 0, numevents;//......//非阻塞获取服务端socket的就绪事件	numevents = aeApiPoll(eventLoop, tvp);//基于返回值处理事件for (j = 0; j < numevents; j++) {aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];//获取事件的mask值int mask = eventLoop->fired[j].mask;//获取该事件是那个客户端套接字int fd = eventLoop->fired[j].fd;int rfired = 0;//如果是读事件则调用rfileProc指针的函数进行命令处理if (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++;}}//......
}

处理redis客户端连接请求

了解整体的模型设计之后,我们就来看看各个事件处理器的具体实现,当epoll轮询的有客户端进行连接时,就会将该事件分发给连接处理器,而连接处理器的核心逻辑为:

  1. 记录套接字信息。
  2. 为该客户端初始化一个redisClient结构体记录其信息。
  3. 将其套接字(socket)的事件注册到epoll中后续进行轮询处理。

在这里插入图片描述

这里我们给出redis的main方法关于redis服务端acceptTcpHandler处理的初始化逻辑,可以看到它会将acceptTcpHandler和服务端套接字的AE_READABLE进行绑定:

 for (j = 0; j < server.ipfd_count; j++) {//为服务端套接字绑定acceptTcpHandler连接处理器if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL) == AE_ERR){//......}}

这里我们也给出acceptTcpHandler处理器的实现逻辑,可以看到其内部的处理逻辑本质获取套接字的文件描述符,并基于这个文件描述符fd为其进行初始化生成redisClient并将事件注册到epollepoll进行轮询处理:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {//......while(max--) {//获取套接字的文件描述符cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);//......//为该套接字进行初始化,并为其生产客户端对象进行管理,并将客户端读写事件交给epollacceptCommonHandler(cfd,0);}
}

步入acceptCommonHandler的逻辑我们即可看到创建redisClient的方法createClient,从核心逻辑可以看出如果redis客户端对象创建失败,它会直接关闭套接字。同理如果创建的redis客户端达到我们上文maxclients的上限也会将其释放并调用write方法提交写事件告知客户端已达上限:

static void acceptCommonHandler(int fd, int flags) {redisClient *c;//为客户端套接字创建redisClient对象并注册事件到epoll中if ((c = createClient(fd)) == NULL) {redisLog(REDIS_WARNING,"Error registering fd event for the new client: %s (fd=%d)",strerror(errno),fd);close(fd); /* May be already closed, just ignore errors */return;}//.......//客户端已达上线输出错误并释放客户端if (listLength(server.clients) > server.maxclients) {char *err = "-ERR max number of clients reached\r\n";//输出错误if (write(c->fd,err,strlen(err)) == -1) {/* Nothing to do, Just to avoid the warning... */}//自增失败连接数server.stat_rejected_conn++;//释放客户端对象freeClient(c);return;}server.stat_numconnections++;c->flags |= flags;
}

最后再来说说创建客户端的处理逻辑,如我们上文所说就是将客户端读事件注册到epoll中,让epoll进行轮询并分发处理,完成该操作后再初始化客户端的各种基础信息:

redisClient *createClient(int fd) {redisClient *c = zmalloc(sizeof(redisClient));//......//将当前客户端套接字读请求即AE_READABLE事件注册到epoll中,并绑定命令处理器readQueryFromClientif (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR){close(fd);zfree(c);return NULL;}}//初始化客户端对象各种参数selectDb(c,0);c->id = server.next_client_id++;c->fd = fd;//.......return c;
}

客户端命令读取与回复

经过上述的处理后,redis客户但就和服务端建立连接,每当客户端发起各种指令操作时,redis的epoll就会轮询到这个客户端套接字的读事件,并将其交给命令处理器处理,完成后将处理结果交给命令回复处理器提交写事件,下次epoll轮询到这个事件就会将结果交给redis客户端

在这里插入图片描述

我们再次给出事件轮询的代码片段,该片段位于ae.c文件下,它会调用aeApiPoll轮询所有套接字对应的fd是否有事件,以客户端命令请求为例,该方法就会返回redis客户端套接字的事件对象,然后计算得到是AE_READABLE事件,就将其交给readQueryFromClient处理器处理,完成后会将处理结果即命令回复交给写处理器sendReplyToClient中,下次aeApiPoll就会轮询到该事件并将其回复给redis客户端:

	//轮询所有套接字查看是否有就绪事件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;//如果是AE_READABLE则说明客户端发起命令,调用rfileProc走到readQueryFromClient方法,完成后生成写事件下次循环时就会走到下方sendReplyToClient的处理逻辑if (fe->mask & mask & AE_READABLE) {rfired = 1;fe->rfileProc(eventLoop,fd,fe->clientData,mask);}//读请求的处理结果就会走到AE_WRITABLE的处理器sendReplyToClient将结果写回客户端if (fe->mask & mask & AE_WRITABLE) {if (!rfired || fe->wfileProc != fe->rfileProc)fe->wfileProc(eventLoop,fd,fe->clientData,mask);}processed++;}

小结

自此我们将redis单线程的reactor模型以及对应的文件驱动设计都分析完毕,希望对你有所帮助。

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

在这里插入图片描述

参考

《redis设计与实现》

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

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

相关文章

学会python——获取文件信息(python实例八)

目录 1、认识Python 2、环境与工具 2.1 python环境 2.2 Visual Studio Code编译 3、获取文件信息 3.1 代码构思 3.2 代码示例 3.3 运行结果 4、总结 1、认识Python Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的…

【星环社区版TDH2024年度大事件】全新版本?全新组件?性能提升10倍?

TDH社区版家族迎来新成员 不知不觉社区版已经陪伴大家将近两年的时间了&#xff0c;在这两年里收获到了很多认可&#xff0c;同时也收获到了一些建议与意见&#xff0c;比如资源成本的问题。在去年我们发布了TDH社区开发版&#xff0c;仅需单台服务器即可一键安装部署Inceptor…

创新实训2024.05.01日志:document-loaders

在建立易学知识库的过程中&#xff0c;仅仅有向量数据库以及词嵌入模型、分词器是不够的&#xff0c;因为我们有大量的非结构化文本&#xff08;如doc,pdf&#xff09;或者是图片需要上传&#xff08;例如pdf里面有图片&#xff09;&#xff0c;此时词嵌入无法直接向向量数据库…

Uniapp获取具体地理位置

使用uniapp自带uni.getLocation获取当前定位经纬度 再调用高德逆地理编码API&#xff0c;查到具体位置信息 https://restapi.amap.com/v3/geocode/regeo?location${longitude},${latitude}&key${key}&extensionsall 但是个人申请的key&#xff0c;有配额限制 最多每…

LabVIEW程序闪退问题

LabVIEW程序出现闪退问题可能源于多个方面&#xff0c;包括软件兼容性、内存管理、代码质量、硬件兼容性和环境因素。本文将从这些角度进行详细分析&#xff0c;探讨可能的原因和解决方案&#xff0c;并提供预防措施&#xff0c;以帮助用户避免和解决LabVIEW程序闪退的问题。 1…

qmt量化交易策略小白学习笔记第44期【qmt编程之期货行情数据】

qmt编程之获取期货行情数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取行情数据 提示 使用该接口时&#xff0c;需要先订阅实时行情(subscribe_quote)或下载过历史行情(download_hi…

k8s中 docker和containerd 镜像相互导入导出

containerd镜像导出并导入docker 1 查看containerd 本地镜像列表 crictl images 2 containerd 导出本地镜像到当前目录下&#xff08;注意&#xff1a; 导出导入需要指定镜像平台类型 --platform&#xff09; ctr -n k8s.io images export nacos-server-24-06-30-13-02-…

openGauss开发者大会、华为云HDC大会举行; PostgreSQL中国技术大会7月杭州开启

重要更新 1. openGauss Developer Day本周五于北京举行&#xff0c;大会聚集了相关行业专家、用户、伙伴和开发者&#xff0c;分享给予openGauss的联合创新成果和实践案例。([2] ) &#xff1b;华为云 HDC 2024本周五于东莞松山湖举行&#xff0c;主题演讲主要覆盖鸿蒙、AI ([3…

Vue3 + Ant-Design 中 a-date-picke 实现选择切换年份 没有鼠标光标,输入框内自带‘年’

效果图&#xff1a; 效果图 <a-date-picker ref"datePicker" v-model:value"year" picker"year" value-format"YYYY年" format"YYYY年" :bordered"false" :allowClear"false" inputReadOnly change&…

【前端项目笔记】3 用户管理

用户管理相关功能实现 涉及表单、对话框、Ajax数据请求 基本页面 用户列表开发 在router.js中导入Users.vue 解决用户列表小问题 选中&#xff08;激活&#xff09;子菜单后刷新不显示高亮 给二级菜单绑定单击事件&#xff0c;点击链接时把对应的地址保存到sessionSto…

vlan技术--交换机实现局域网分割(Access模式trunk模式)

自作笔记... 目录 vlan技术--交换机连接pc实现局域网分割(Access模式) PC SW1 结果 vlan技术--交换机连接pc实现局域网分割(trunk模式) vlan技术--交换机连接pc实现局域网分割(Access模式) 交换机先创建vlan. 交换机分别进入接口 (配置好连接模式, 连接的vlan) PC SW1 …

Set集合系列——Set、HashSet、LinkedHashset、TreeSet

Set系列的公共特点&#xff1a;无重复、无索引&#xff0c;不可用普通for循环&#xff0c;API和Collection重复 HashSet&#xff1a;采取哈希表存取数据 哈希表组成&#xff1f; JDk8之前&#xff1a;数组链表&#xff0c; JDK8以后&#xff1a;数组链表红黑树 哈希值&#…

简单高效的盈利策略,昂首资本推荐价格行为交易

有没有这样一种简单高效的盈利策略&#xff0c;不仅易于新手掌握&#xff0c;也是专业人士的常用利器?当然有了&#xff0c;就是Anzo Capital昂首资本今天推荐的价格行为交易。价格行为交易以其透明清晰的市场视角受到交易员的青睐&#xff0c;它如实反映了市场的真实动态&…

Ubuntu下安装docker

一、docker安装说明 解决官方源无法下载的问题 二、使用步骤 1.更新软件包索引 sudo apt-get update2.安装必要的软件包&#xff0c;以允许apt通过HTTPS使用仓库 sudo apt-get install apt-transport-https ca-certificates curl software-properties-common3.添加Docker的…

功能测试 之 单模块测试----购物车模块

1.需求分析 &#xff08;1&#xff09;购物车显示 1.若未登录&#xff0c;提示登录&#xff0c;提示文案“购物车内暂时没有商品&#xff0c;登录后将显示您之前加入的商品” 2.若已登录&#xff0c;购物车没有商品&#xff0c;提示去购物。 未登录状态 已登录状态 3.购物车有…

CVPR2024|UniPAD:一种自动驾驶的统一的预训练范式

本文章仅用于学术分享 论文标题丨 UniPAD: A Universal Pre-training Paradigm for Autonomous Driving 论文地址丨 https://arxiv.org/abs/2310.08370 代码地址 | https://github.com/Nightmare-n/UniPAD 关注「AI前沿速递」公众号&#xff0c;获取更多前沿资讯 01总览 这…

Spring Clude 是什么?

目录 认识微服务 单体架构 集群和分布式架构 集群和分布式 集群和分布式区别和联系 微服务架构 分布式架构&微服务架构 微服务的优势和带来的挑战 微服务解决方案- Spring Cloud 什么是 Spring Cloud Spring Cloud 版本 Spring Cloud 和 SpringBoot 的关系 Sp…

「51媒体」食品展览展会活动,媒体邀约资源有哪些?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 食品展览展会活动在媒体邀约方面拥有丰富的资源&#xff0c;可以吸引各类媒体的关注和报道。以下是一些常见的媒体邀约资源&#xff1a; 1. 行业媒体&#xff1a; 专业食品杂志&#xff…

可编程非线性RCD负载原理与应用

可编程非线性RCD负载&#xff08;Resistor-Capacitor-Diode&#xff09;是一种电子元件&#xff0c;其电阻、电容和二极管的特性可以通过编程进行控制和调整。这种负载广泛应用于电力系统、通信设备、电子设备等领域&#xff0c;具有很高的实用价值。 RCD负载的基本原理是利用电…

超声波清洗机的优势到底有哪些?四款精良爆品总结安利,质量放心

眼镜是现代人生活中的必备物品&#xff0c;但是很多人可能对于如何正确清洗眼镜感到困惑。传统的清洗方法可能会在清洗过程中对眼镜造成损坏&#xff0c;例如使用普通肥皂或清水清洗时容易划伤镜片。为了解决这个问题&#xff0c;家用眼镜超声波清洗机应运而生。超声波清洗机利…