redis源码分析之IO多路复用

news2024/11/22 4:36:42

文章目录

    • 1、简述
    • 2、多路复用的三个函数
    • 3、创建epoll实例
    • 4、绑定端口、监听端口
    • 5、向epoll实例注册连接事件
    • 6、从epoll实例中获取就绪的事件

1、简述

众所周知,redis是一款抗高并发的利器,据官方压测,单机可达10万qps。但背后实际处理命令的线程只有一条,这听上去其实挺匪夷所思的,因为在我们的日常开发中,说到高并发,多线程是一个非常常用的解决方案。那redis凭什么靠一条线程,就能支持高并发呢?最主要的原因,就是标题所说的IO多路复用,IO多路复用是怎么做的呢?这是老八股了,IO多路复用,背后依赖的是多路复用的函数,有select、poll、epoll,linux默认使用的是epoll函数,redis把客户端连接通过epoll函数给到内核,内核监听到连接有可读写的事件,就将该事件返回redis进行处理。那具体的实现细节呢?redis怎么给的内核,内核又怎么返回的?

2、多路复用的三个函数

epoll函数由3个函数组合来完成多路复用这件事。分别是:
epoll_create、epoll_ctl、epoll_wait
1)、epoll_create:创建epoll实例
2)、epoll_ctl:将连接对应的socket描述符注册到epoll实例中
3)、epoll_wait:获取epoll实例中可读写的描述符
画一个简单的流程图串一下这三个函数的作用
请添加图片描述
从图中可以看出,redis在启动的时候,先是通过epoll_create函数创建epoll实例,然后绑定端口、监听端口,然后通过epoll_ctl函数注册连接事件,最后会搞一个死循环,通过epoll_wait函数获取可读写的事件(每一个事件对应的都是一个可读写的客户端连接)
铺垫完上面的流程,我们看一下源码。redis的启动源码在server.c文件的main方法中,main方法是redis启动的入口,其中有很多流程,我们只关注我们这个流程会用到的函数。

3、创建epoll实例

首先是通过内核提供的epoll_create函数创建epoll实例,这个流程入口在initServer方法中.

void initServer(void) {
    ......

    //创建epoll实例
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    if (server.el == NULL) {
        serverLog(LL_WARNING,
            "Failed creating the event loop. Error message: '%s'",
            strerror(errno));
        exit(1);
    }
    ......
}

aeCreateEventLoop是创建epoll实例的入口,我们进入这个方法。

aeEventLoop *aeCreateEventLoop(int setsize) {
    ......
    
    if (aeApiCreate(eventLoop) == -1) goto err;
    
    ......
}

其中又调用了一个aeApiCreate方法,这个方法是对epoll_create函数做了一层封装,我们继续进入aeApiCreate方法。

static int aeApiCreate(aeEventLoop *eventLoop) {
    ......
    
    //创建epoll实例
    //这里的1024并不是说epoll函数只能监听1024个描述符.因为在2.6.8内核之后,内核维护的是一个动态的队列,理论上我们可以一直添加描述符
    
    state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
    
    ......
}

4、绑定端口、监听端口

这里就看到了我们想找的epoll_create函数。
创建完epoll实例后,接下来就是绑定端口、监听端口。
这部分的代码也是在initServer方法中,就在创建epoll实例的下方

void initServer(void) {
    
    ......
    //创建epoll实例
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    if (server.el == NULL) {
        serverLog(LL_WARNING,
            "Failed creating the event loop. Error message: '%s'",
            strerror(errno));
        exit(1);
    }
    server.db = zmalloc(sizeof(redisDb)*server.dbnum);

    ......
    
    //绑定、监听端口
    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
        exit(1);
    ......
}

绑定、监听端口的逻辑在listenToPort方法中,该方法的入参有3个值,第一个就是要绑定、监听的端口,默认是6379。第二个值是描述符,第三个是描述符的数量。这个时候,后面这两个参数还没值,需要到listenToPort方法中赋值。

int listenToPort(int port, int *fds, int *count) {
    ......
    //绑定IPV6
    fds[*count] = anetTcp6Server(server.neterr,port,NULL,server.tcp_backlog);

    ......
    //绑定IPV4
    fds[*count] = anetTcpServer(server.neterr,port,NULL,server.tcp_backlog);

    ......
    (*count)++;
}

所以最终fds数组一共赋值2个值。count赋值2

5、向epoll实例注册连接事件

这个逻辑还是在initServer方法中。server.ipfd_count的值就是上面的那个count值,是2。所以这个循环会执行2次,注册2个连接事件,一个IPV4、一个IPV6
aeCreateFileEvent,是一个非常重要的方法,是用来创建事件的。该方法有5个入参。
第一个是redis对应epoll实例的结构体,第二个是需要注册的描述符,第三个是需要注册的事件类型,第四个是事件触发后的回调函数,最后一个是客户端数据。我们是注册连接事件,所以不会有客户端数据,此时客户端还没有连接redis

void initServer(void) {
    ......
    //注册连接事件
    
    for (j = 0; j < server.ipfd_count; j++) {
    
    ......

    if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,acceptTcpHandler,NULL) == AE_ERR)
    
    ......
    }

    ......
}

我们进入aeCreateFileEvent方法,

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    ......
    
    //aeApiAddEvent函数内部调用epoll_ctl函数
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    ......
    //将acceptTcpHandler回调函数挂到当前连接事件上
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    
    ......
}

aeCreateFileEvent主要就是做两件事,注册连接事件、给事件挂回调函数,回调函数就是acceptTcpHandler。aeApiAddEvent是对epoll_ctl函数的封装。我们进入其中看一下

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    ......
    //调用epoll的epoll_ctl函数注册事件,一共4个参数。
    //1、epoll实例
    //2、要执行的操作类型,添加事件还是修改修改事件。第一次肯定是添加事件
    //3、要监听的文件描述符
    //4、epoll_event类型变量
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    ......
}

这里,我们就看到了epoll_ctl函数。

6、从epoll实例中获取就绪的事件

这个获取就绪事件的动作,是在main方法的aeMain函数中。

int main(int argc, char **argv) {
    ......
    //执行aeMain函数开启事件循环处理框架
    aeMain(server.el);
    ......
}

我们进入aeMain函数。

void aeMain(aeEventLoop *eventLoop) {
    //只要redis实例没有停止,while循环就会一直执行
    eventLoop->stop = 0;
    //redis服务是否停止的标志,如果stop值变为1,说明redis服务停止了
    while (!eventLoop->stop) {
        ......
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}

我们看到获取就绪的事件函数是aeProcessEvents,我们进入其中看一下

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
        ......
        //调用多路复用API
        numevents = aeApiPoll(eventLoop, tvp);
        ......
}

可以看到一个aeApiPoll函数,该函数是对epoll_wait函数的封装,我们继续进入aeApiPoll函数。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    //等待有可读写的事件发生.返回值为可读写的事件数量
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    if (retval > 0) {
        int j;
        //获得监听到的事件数量
        numevents = retval;
        //针对每一个就绪的事件进行处理
        for (j = 0; j < numevents; j++) {
            //保存事件信息
            int mask = 0;

            //获取到当前就绪的这个事件
            struct epoll_event *e = state->events+j;

            //EPOLLIN代表epoll模型的读事件,这一行代码的意思是将epoll的读事件映射到redis事件驱动框架的读事件
            if (e->events & EPOLLIN) mask |= AE_READABLE;

            //EPOLLOUT代表epoll模型的写事件,这一行代码的意思是将epoll的写事件映射到redis事件驱动框架的写事件
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;

            //EPOLLERR:错误事件,表示文件描述符对应套接字出错
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;

            //将epoll模型中已就绪的描述符映射到redis事件循环框架的就绪事件数组中
            eventLoop->fired[j].fd = e->data.fd;

            //给已就绪的事件设置事件类型
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

在其中,我们看到了epoll_wait函数,epoll_wait一共四个入参。
第一个是:要监听的描述符集合,第二个是要监听的事件类型,第三个是要监听的描述符数量,第四个是等待结果返回的超时时间
返回了结果后,下面的逻辑就是处理这个就绪的事件,这个方法是redis IO多路复用的关键所在,redis不停的接收客户端请求,这个方法是主要逻辑,我给每一行代码都加了注释,可以细看一下。
redis的这部分多路复用逻辑写的很清晰,可以认真梳理一下,对多路复用的原理会有更清晰的认识。
文章参考了极客时间的redis源码课程《redis源码剖析与实战》,文章写的挺好,有兴趣的小伙伴可以去看看。

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

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

相关文章

字典与数组第八讲:工作表数据计算时为什么要采用数组公式(二)

《VBA数组与字典方案》教程&#xff08;10144533&#xff09;是我推出的第三套教程&#xff0c;目前已经是第二版修订了。这套教程定位于中级&#xff0c;字典是VBA的精华&#xff0c;我要求学员必学。7.1.3.9教程和手册掌握后&#xff0c;可以解决大多数工作中遇到的实际问题。…

具有自主产权的SaaS门店收银系统全套源码输出

PHPMysql前后端分离&#xff0c; 小程序线上商城&#xff1b; 进销存管理库存盘点&#xff0c; 多仓库库存调拨&#xff0c; 会员系统。 消费者扫码查价系统。

外卖行业如何借助微信管理系统实现高效运营

摘要&#xff1a;本文将介绍微信管理系统在外卖行业的应用&#xff0c;包括聚合聊天、朋友圈营销和群发功能。通过这些功能&#xff0c;外卖商家可以更高效地管理订单、与客户沟通、推广品牌和增加销售额。 一、引言 随着外卖行业的快速发展&#xff0c;竞争也日益激烈。为了…

前端面试题之HTML篇

1、src 和 href 的区别 具有src的标签有&#xff1a;script、img、iframe 具有href的标签有&#xff1a;link、a 区别 src 是source的缩写。表示源的意思&#xff0c;指向资源的地址并下载应用到文档中。会阻塞文档的渲染&#xff0c;也就是为什么js脚本放在底部而不是头部的…

Vert.x学习笔记-Vert.x的基本处理单元Verticle

Verticle介绍 Verticle是Vert.x的基本处理单元&#xff0c;Vert.x应用程序中存在着处理各种事件的处理单元&#xff0c;比如负责HTTP API响应请求的处理单元、负责数据库存取的处理单元、负责向第三方发送请求的处理单元。Verticle就是对这些功能单元的封装&#xff0c;Vertic…

数据中心系统解决方案

设计思路 系统设计过程中充分考虑各个子系统的信息共享要求&#xff0c;对各子系统进行结构化和标准化设计&#xff0c;通过系统间的各种联动方式将其整合成一个有机的整体&#xff0c;使之成为一套整体的、全方位的数据中心大楼综合管理系统&#xff0c;达到人防、物防和技防…

MySQL8.0.26-unbuntu版安装

MySQL8.0.26-ubuntu版安装 在这里会有一个坑&#xff0c;就是我在安装的时候,是按照另外一种版本的安装&#xff0c;报错没有rpm这个包&#xff0c;然后我就去下载&#xff0c;然后就报错 E: 无法定位软件包 &#xff0c;害的我找了好久的资料&#xff0c;一直没有解决&#x…

2023-11-04:用go语言,如果n = 1,打印 1*** 如果n = 2,打印 1*** 3*** 2*** 如果n = 3,打印

2023-11-04&#xff1a;用go语言&#xff0c;如果n 1&#xff0c;打印 1*** 如果n 2&#xff0c;打印 1***3*** 2*** 如果n 3&#xff0c;打印 1***3*** 2***4*** 5*** 6*** 如果n 4&#xff0c;打印 1***3*** 2***4*** 5*** 6***10** 9*** 8*** 7*** 输入…

SoftwareTest5 - 你就只知道功能测试吗 ?

你就只知道功能测试吗 ? 一 . 按照测试对象划分1.1 文档测试1.2 可靠性测试1.3 容错性测试1.4 安装卸载测试1.5 内存泄漏测试1.6 弱网测试 二 . 按是否查看代码划分2.1 黑盒测试2.2 白盒测试2.3 灰盒测试 三 . 按照开发阶段划分3.1 单元测试3.2 集成测试3.3 冒烟测试3.4 系统测…

如何通过智能管理箱实现高效文件管理:关键字轻松修改文件名

在信息化时代&#xff0c;文件管理变得尤为重要。智能管理箱已经成为我们生活中不可或缺的一部分。它可以帮助我们高效地管理各种文件&#xff0c;使得我们的工作和生活更加便捷。是一种高效的文件管理工具&#xff0c;可以帮助我们轻松地整理和分类文件&#xff0c;提高工作效…

【算法】昂贵的聘礼(dijkstra算法)

题目 年轻的探险家来到了一个印第安部落里。 在那里他和酋长的女儿相爱了&#xff0c;于是便向酋长去求亲。 酋长要他用 10000 个金币作为聘礼才答应把女儿嫁给他。 探险家拿不出这么多金币&#xff0c;便请求酋长降低要求。 酋长说&#xff1a;”嗯&#xff0c;如果你能够替我…

SpringBoot+AOP+自定义注解,优雅实现日志记录

文章目录 前言准备阶段1、数据库日志表2、自定义注解编写3、AOP切面类编写4、业务层4.1、Service 层&#xff1a;4.2 Service 实现层&#xff1a; 5、测试 前言 首先我们看下传统记录日志的方式是什么样的&#xff1a; DeleteMapping("/deleteUserById/{userId}") …

【C语言:函数栈帧的创建与销毁】

文章目录 前言一、前期准备1.寄存器2.汇编指令3.测试代码 二、解开函数栈帧的神秘面纱1.栈帧大体轮廓2.main函数栈帧的创建3.main函数内执行有效代码4.烫烫烫5.函数参数的传递6.add函数栈帧的创建7.add函数内执行有效代码8.add是如何获得参数的9. add函数栈帧的销毁10.main函数…

IDEA中如何移除未使用的import

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是全栈工…

@Slf4j将日志记录到磁盘和数据库

文章目录 1、背景介绍2、存本地2.1、配置文件2.2、使用 3、存数据库3.1、配置文件改造3.2、过滤器编写3.3、表准备3.4、添加依赖3.5、测试 4、优化4.1、日志定期删除 1、背景介绍 现在我一个SpringBoot项目想记录日志&#xff0c;大概可以分为下面这几种&#xff1a; 用户操作…

速学数据结构 | 链表实现队列究竟有什么优势?

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《速学数据结构》 《C语言进阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! &#x1f4cb; 前言 &#x1f308;hello&#xff01; 各位宝子们大家好啊&#xff0c;栈区的实现我们前面已经讲了&#…

0005Java安卓程序设计-ssm基于Android的网店系统

文章目录 **摘要**目录系统设计开发环境 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 摘要 随着Internet的发展&#xff0c;人们的日常生活已经离不开网络。未来人们的生活与工作将变得越来越数字化&#xff0c;网络化和电子化。网上管…

系统提示缺少或找不到emp.dll文件的详细解决方案

我今天打开一款《游戏》。然而&#xff0c;在游戏中遇到了一个非常棘手的问题&#xff1a;游戏报错找不到emp.dll,无法继续执行代码。这让我们非常苦恼&#xff0c;因为这个问题严重影响了我们的游戏体验。 在经过一番努力之后&#xff0c;我终于找到了4个解决方法&#xff0c…

要讨个公道,要分辨真假

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

Qt利用VCPKG和CMake和OpenCV和Tesseract实现中英文OCR

文章目录 1. 开发平台2. 下载文件2.1 下载安装 OpenCV 库2.2 下载安装 Tesseract-OCR库2.3 下载训练好的语言包 3. CMakeLists.txt 内容4. Main.cpp4.1 中英文混合OCR 5. 在Qt Creator 中设置 CMake vcpkg5.1 在初始化配置文件里修改5.2 在构建配置里修改 说明&#xff1a;在Q…