Redis高性能设计之epoll和IO多路复用深度解析

news2024/11/28 11:45:18

一 多路复用

并发多客户端连接, 在多路复用之前最简单和典型的方案:同步阻塞网络IO模型
这种模型的特点就是用一个进程来处理一个网络连接(一个用户请求),比如一段典型的示例代码如下:

//直接调用recv函数从一个socket上读取数据
int main(){
	...
	recv(sock,...)//从用户角度来看非常简单,一个recv一用,要接收的数据就到我们手里了。
}

这种方式的优点就是非常容易让人理解,写起来非常的自然,符合人的直线型思维。
缺点就是性能差,每个用户请求到来都得占用一个进程来处理,来一个请求就要分配一个进程跟进处理。
类似一个学生配一个老师,一位患者配一个医生,可能吗?进程是一个很笨重的东西,一台服务器上创建不了多少个进程。

进程在Linux上是一个不小的开销,先不说创建,光是上下文切换一次就得几个微妙。所以为了高效地对海量用户提供服务,必须要让一个进程能同时处理很多个tcp连接才行。现在假设一个进程保持了10000条连接,那么如何发现哪条连接上有数据可读,哪条连接可写呢?
我们当然可以采用循环遍历的方式来发现IO时间,但这种方式太低级了。
我们希望有一种更高效的机制,在很多连接中的某条上有IO是事件发生的时候直接快速地把它找出来。
其实这个事情Linux操作系统已经替我们做好了,它就是我们所熟知的IO多路复用机制,这里的复用指的就是对进程的复用

二 I/O多路复用模型

2.1 介绍

  • I/O:网络I/O
  • 多路:多个客户端连接(连接就是套接字描述符,即socket或者channel),指的是多条TCP连接
  • 复用:用一个进程来处理多条的连接,使用单进程就能够实现同时处理多个客户端的连接。
  • 总结:实现了用一个进程处理大量的用户连接,IO多路复用类似一个规范和接口落地实现。可以分select->poll->epoll三个阶段来描述。

2.2 Redis单线程如何处理那么多客户端连接,为什么单线程,为什么快?

Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器
在这里插入图片描述
在这里插入图片描述

三 Unix网络编程中的术语

3.1 同步

调用者要一直等待调用结果的通知后才能进行后续的执行,现在就要,我可以等,等出结果为止。

3.2 异步

指被调用方先返回应答让调用者先回去。然后再计算调用结果,计算完最终结果后再通知并返回给调用方。异步调用要想获得结果一般通过回调。

3.3 同步与异步的理解

同步、异步的讨论对象是被调用者(服务提供者),重点在于获得调用结果的消息通知方式上。

3.4 阻塞

调用方一直在等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干。

3.5 非阻塞

调用在发出去后,调用方先去忙别的事情,不会阻塞当前进/线程,而会立即返回。

3.6 阻塞与非阻塞的理解

阻塞、非阻塞的讨论对象是调用者(服务请求者),重点在于等消息时候的行为,调用者是否能干其它事。

3.7 总结

  • 同步阻塞
  • 同步非阻塞
  • 异步阻塞
  • 异步非阻塞

四 Unix网络编程中的五种IO模型

4.1 Blocking IO 阻塞IO

在这里插入图片描述
recvfrom():用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
在这里插入图片描述

4.1.1 ServiceBIO

/**
 * @author seapp
 * @date 2023/6/7 9:11
 */
public class RedisServerBIO {

    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(6399 );

        while (true){
            System.out.println("----111 等待连接");
            Socket socket = serverSocket.accept();
            System.out.println("-----222   连接成功");

            //获取输入流
            InputStream inputStream = socket.getInputStream();
            int length = -1;
            byte[] bytes = new byte[1024];
            System.out.println("-----333 等待读取");
            while ((length = inputStream.read(bytes)) != -1){
                System.out.println("---- 444 读取成功" + new String(bytes,0,length));
                System.out.println("======================" + "\t" + IdUtil.simpleUUID());
                System.out.println();
            }
            //资源关闭
            inputStream.close();
            socket.close();
        }

    }

}

4.1.1 client

/**
 * @author seapp
 * @date 2023/6/7 9:14
 */
public class RedisClient1 {


    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1", 6399);
        OutputStream outputStream = socket.getOutputStream();

        while (true){
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if(string.equalsIgnoreCase("quit")){
                break;
            }
            outputStream.write(string.getBytes());
            System.out.println("---------RedisClient01 input quit keyword to finish----------");
        }

        outputStream.close();;
        socket.close();
    }

}

4.1.3 上述模型存在的问题

在这里插入图片描述

上面的模型存在很大的问题,如果客户端与服务端建立了连接。如果这个连接的客户端迟迟不发数据,线程就会一直阻塞在read()方法上。这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好。
解决方案:
利用多线程,只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个具体线程上面不堵塞主线程,就能操作多个socket了。哪个线程中的socket有数据,就读哪个socket。
程序服务端只负责监听是否有客户端连接,使用accept()阻塞。
任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理。

4.1.4 多线程修改

/**
 * @author seapp
 * @date 2023/6/7 9:11
 */
public class RedisServerBIO {


    public static void main(String[] args) throws IOException {


        ServerSocket serverSocket = new ServerSocket(6399);


        while (true) {
            System.out.println("----111 等待连接");
            Socket socket = serverSocket.accept();
            System.out.println("-----222   连接成功");


            new Thread(() -> {
                try {
                    //获取输入流
                    InputStream inputStream = socket.getInputStream();
                    int length = -1;
                    byte[] bytes = new byte[1024];
                    System.out.println("-----333 等待读取" + "\t" + IdUtil.simpleUUID());
                    while ((length = inputStream.read(bytes)) != -1) {
                        System.out.println("---- 444 读取成功" + new String(bytes, 0, length));
                        System.out.println();
                    }

                    //资源关闭
                    inputStream.close();
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }, Thread.currentThread().getName()).start();
            
        }

    }

}

4.1.5 多线程模型存在的问题

多线程模型,每来一个客户端就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。在操作系统中用户态不能直接开辟线程,需要调用内核来创建一个线程。这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。

解决方案:

  • 使用线程池:这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行。
  • NIO(非阻塞式IO)方式:因为read()方法阻塞了,所以要开辟多个线程,如果什么方法能够使read()方法不堵塞,这应就不用开辟多个线程了,这就用到了另一个IO模型,NIO(非阻塞式IO)。

4.1.6 小总结

在阻塞式IO模型中,应用程序在从调用recvfrom开始到它返回有数据包准备好这段时间是阻塞的,recvfrom返回成功后,应用进程才能开始处理数据包。
每一个线程分配一个连接,必然会产生多个。既然是多个socket连接必然需要放进容器,进行统一管理。

4.2 NoneBlocking IO 非阻塞IO

4.2.1 NIO

在这里插入图片描述
在这里插入图片描述
在NIO模式中,一切都是非阻塞的:
accept()方法是非阻塞的,如果没有客户端连接,就返回无连接标识。
read()方法是非阻塞的,如果read()方法读取不到数据就返回空闲标识,如果读取到数据时只阻塞read()方法读数据的时间。
在NIO模型中,只有一个线程:
当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了

4.2.2 ServiceNIO实现

/**
 * @author seapp
 * @date 2023/6/7 14:19
 */
public class RedisServiceNIO {

    static ArrayList<SocketChannel> socketList = new ArrayList<>();

    static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);


    public static void main(String[] args) throws IOException {

        System.out.println("-------------RedisServiceNIO 启动等待中.....");
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 6399));
        serverSocket.configureBlocking(false);//设置为非阻塞状态

        while (true) {
            for (SocketChannel element : socketList) {
                int read = element.read(byteBuffer);
                if (read > 0) {
                    System.out.println("------读取数据: " + read);
                    byteBuffer.flip();
                    byte[] bytes = new byte[read];
                    byteBuffer.get(bytes);
                    System.out.println(new String(bytes));
                    byteBuffer.clear();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            SocketChannel channel = serverSocket.accept();
            if (channel != null) {
                System.out.println("-------成功连接----------");
                channel.configureBlocking(false);//设置为非阻塞
                socketList.add(channel);
                System.out.println("----------socketList size: " + socketList.size());
            }
        }
    }
}

在这里插入图片描述

4.2.3 NIO的优缺点

缺点:
NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,但是还存在2个问题。

  • 这个模型在客户端少的时候十分好用,但是客户端如果很多。比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会遍历一万个socket,就会做很多无用功,每次遍历遇到read 返回 -1时,仍然是一次浪费资源的系统调用。
  • 而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大因为这些问题的存在。

优点:

  • 不会阻塞在内核的等待数据过程,每次发起的I/O请求可以立即返回,不用阻塞等待,实时性较好。

但是轮询将会不断的询问内核,这将占用大量的CPU时间,系统资源利用率较低,所以一般web服务器不使用这种I/O模型。

小总结:
让Linux内核搞定上述需求,我们将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题。IO多路复用应运而生,也即将上述工作直接放进Linux内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的。

4.3 IO multiplexing IO多路复用

4.3.1 定义

在这里插入图片描述
I/O multiplexing 这里面的multiplexing指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流,目的是尽量多的提高服务器的吞吐能力。

在这里插入图片描述
FileDescriptor: 文件描述符(File descriptor -> fd)是计算机科学中的一个术语,是一个用于表述指向文件的应用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

IO多路复用
在这里插入图片描述
IO multiplexing就是我们说的select,poll,epoll,有些技术书籍也称这种IO方式为event driven IO事件驱动IO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象并同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次new一个线程),这样可以大大节省系统资源。所以,IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符(套接字描述符)而这些文件描述符其中的任意一个进入就绪状态,select、poll、epoll等函数就可以返回。

将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进行或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。

4.3.2 作用

Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到事件分派器,事件分派器将事件分发给事件处理器。
在这里插入图片描述

4.3.2 Reactor

基于IO复用模型,多个连接共用一个阻塞对象,应用程序只需要在上一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatch模式。即IO多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术。
在这里插入图片描述
Reactor模式中有2个关键组成:

  • Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。
  • Handlers处理程序执行IO事件要完成的实际事件。Reactor通过调度适当的处理程序来响应IO事件,处理程序执行非阻塞操作。

4.3.3 IO多路复用的select方法

在这里插入图片描述
缺点:
在这里插入图片描述

4.3.4 IO多路复用的poll方法

在这里插入图片描述
优点:

  • poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client。它和select的主要区别就是,去掉了select只能监听1024个文件描述符的限制。
  • 当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回零,实现了pollfd数组的重用。

缺点:
本质上还是select的方法。

  • pollfds数组拷贝到了内核态,仍然有开销。
  • poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历。

4.3.5 IO多路复用的epoll方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

## 4.4 signal driven IO 信号驱动IO
## 4.5 asynchronous IO 异步IO

五种IO模型的比较

在这里插入图片描述

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

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

相关文章

【Java】JAVASE面向对象知识点总结

面向对象 概念 现实生活&#xff1a; ​ 类&#xff1a;抽象的概念&#xff0c;把具有相同特征和操作的事物归为一类 ​ 先有实体&#xff0c;再有类的概念 代码世界&#xff1a; ​ 类&#xff1a;抽象的概念&#xff0c;把具有相同属性和方法的对象归为一类 ​ 编写顺序&…

epoll的LT与ET模式以及阻塞和非阻塞

文章目录 1、基本概念2、关于在socket和EPOLL中的阻塞与非阻塞3、几种IO模型的触发方式4、代码验证5、总结 1、基本概念 Level_triggered(水平触发)&#xff1a;当被监控的文件描述符上有可读写事件发生时&#xff0c;epoll_wait()会通知处理程序去读写。如果这次没有把数据一次…

Flask-蓝图

1、使用步骤&#xff1a; 创建蓝图 blue Blueprint("myblue01", __name__) 使用蓝图装饰视图函数 blue.route(/) def index():return index 将蓝图注册到app中 from appdemo_blueprint import blue app.register_blueprint(blue) 2、以包的形式使用蓝图 <…

Java007——Java注释学习

围绕以下3点介绍&#xff1a; 1、什么是Java注释&#xff1f; 2、Java注释的作用&#xff1f; 3、Java注释长什么样&#xff0c;以及怎么使用Java注释&#xff1f; 一、什么是Java注释&#xff1f; Java注释是在Java程序中用来描述代码的特殊语句。 注释被忽略并且不被编译器…

PyTorch 深度学习 || 专题六:PyTorch 数据的准备

PyTorch 数据的准备 1. 生成数据的准备工作 import torch import torch.utils.data as Data#准备建模数据 x torch.unsqueeze(torch.linspace(-1, 1, 500), dim1) # 生成列向量 y x.pow(3) # yx^3#设置超参数 batch_size 15 # 分块大小 torch.manual_seed(10) # 设置种子点…

javascript基础三十:JavaScript如何实现上拉加载,下拉刷新?

一、前言 下拉刷新和上拉加载这两种交互方式通常出现在移动端中 本质上等同于PC网页中的分页&#xff0c;只是交互形式不同 开源社区也有很多优秀的解决方案&#xff0c;如iscroll、better-scroll、pulltorefresh.js库等等 这些第三方库使用起来非常便捷 我们通过原生的方…

哈工大 大数据 数据库实验(3) 物理数据库设计--索引结构

指导书: HIT邹老师数据库实验三资源-CSDN文库 导入数据库 操作 两个元组的分布规律 1.查询元组的数据分布 SELECT COUNT(*) FROM Foo; SELECT COUNT(*) FROM Foo WHERE id 0; SELECT COUNT(*) FROM Foo WHERE a 0; SELECT COUNT(*) FROM Foo WHERE b 0; SELECT COUNT(*…

c++ 解析html与htmlcxx库

目录 1&#xff0c;htmlcxx Github 版本源码下载 2&#xff0c;htmlcxx Linux 版本源码下载 3&#xff0c;htmlcxx 解析例子 1&#xff0c;htmlcxx Github 版本源码下载 正如在前一篇文章 c CFile 类 里提到的&#xff0c;我想要从指定的 html 文件里提取…

【裸机开发】镜像文件内部信息构成

由于当前使用的是裸板&#xff0c;没有OS&#xff0c;DDR的初始化、文件保存地址都尚未确定&#xff0c;所以我们生成二进制文件以后&#xff0c;是无法直接放到开发板上运行的。 因此&#xff0c;我们一般会在二进制文件之前加一些头部信息&#xff0c;这些头部信息会告诉开发…

xsync集群分发脚本

脚本&#xff1a; #!/bin/bash #1. 判断参数个数 if [ $# -lt 1 ] thenecho Not Enough Arguement!exit; fi #2. 遍历集群所有机器 for host in hadoop02 hadoop03 doecho $host #3. 遍历所有目录&#xff0c;挨个发送for file in $do#4. 判断文件是否存在if [ -e $file ]th…

文本三剑客awk:命令讲解

awk-命令讲解&#xff1a; 一、awk&#xff1a; 1.定义&#xff1a; &#xff08;1&#xff09;在 Linux/UNIX 系统中&#xff0c;awk 是一个功能强大的编辑工具&#xff0c;逐行读取输入文本&#xff0c;默认以空格或tab键作为分隔符作为分隔。并按模式或者条件执行编辑命令…

006-从零搭建微服务-注册中心(二)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff1a;https://gitee.com/csps/mingyue 文档地址&#xff1a;https://gitee.com/csps/mingyue/wikis 核心依赖 需要注册配置中心的服务引入下面 …

步进电机基本原理及驱动方式详解

步进电机基本原理及驱动方式详解 步进电机相关概念基本原理类型和结构转子结构定子结构 励磁方式ATD9800 驱动使用实例 参考文献 步进电机相关概念 步进电机是一种常用于控制和定位应用的电动机&#xff0c;其独特的工作方式使其在许多领域中都具有广泛的应用。步进电机以其 精…

正态总体下常见的抽样分布

1.正态总体下常见的抽样分布 本人博客&#xff1a;总体分布、样本分布、抽样分布的区别 本人博客&#xff1a;三大抽样分布 正态总体下常见的抽样分布意思是&#xff1a;样本来自服从正态分布的总体中&#xff0c;从样本中抽样后得到的分布 1.1 单个正态总体下的抽样分布&…

【高危】GitLab CE/EE 存在存储型XSS漏洞

漏洞描述 GitLab 是一款基于Git的代码托管、版本控制、协作开发平台。 在 GitLab CE/EE 15.11 至 15.11.6 版本以及 16.0 至 16.0.1 版本中&#xff0c;当GitLab导入GitHub仓库时&#xff0c;如果GitHub仓库中包含由用户构造的恶意JavaScript代码的标签颜色&#xff0c;解析这…

为什么说程序员和产品经理一定要学一学PMP

要回答为什么说程序员和产品经理一定要学一学PMP&#xff1f;我们得先看一下PMP包含的学习内容。PMP新版考纲备考参考资料绝大多数涉及IT项目的敏捷管理理念。主要来源于PMI推荐的10本参考书&#xff1a; 《敏捷实践指南&#xff08;Agile Practice Guide&#xff09;》 《项目…

Java匿名内部类、invoke方法、动态代理

一、匿名内部类 匿名内部类一般作为方法的参数&#xff0c;这个方法的形参为接口&#xff0c;而实参为匿名内部类&#xff08;可以理解为接口的对象&#xff09;并且重写了接口中的方法。 例如&#xff1a; 定义了一个接口Star&#xff1a; 类型为Star的引用数据类型作为方法s…

关于docker中Nacos启动成功访问不了的情况

书接上回&#xff0c;这段时间在忙学成在线的微服务项目&#xff0c;上次出现了IP修改的问题&#xff0c;有了一定的解决方案&#xff0c;复制别人的虚拟机后如何修改IP_SSSEdward的博客-CSDN博客。 这次docker正常启动了&#xff0c;但是出现了访问不了的情况&#xff0c;但是…

短视频矩阵系统源码打包附赠分享

矩阵系统源码主要有三种框架&#xff1a;Spring、Struts和Hibernate。Spring框架是一个全栈式的Java应用程序开发框架&#xff0c;提供了IOC容器、AOP、事务管理等功能。Struts框架是一个MVC架构的Web应用程序框架&#xff0c;用于将数据模型、Web应用程序的用户界面和控制器逻…

【备战秋招】Java异常处理

Java程序在执行过程中所发生的异常事件可分为两类&#xff1a; Error&#xff1a;Java虚拟机无法解决的严重问题。如&#xff1a;JVM系统内部错误、资源 耗尽等严重情况。 比如&#xff1a;StackOverflowError和OOM。一般不编写针对性的代码进行处理。 Exception: 其它因编程错…