Redis的网络模型

news/2024/5/18 22:40:36

Redis是单线程还是多线程?

Redis3.0之前都是单线程

1)如果只是针对于Redis的核心业务部分(命令处理),答案是单线程

2)如果是说整个redis,那么就是多线程

在Redis的版本迭代过程中,在两个非常重要的时间节点上引入了对多线程的支持:

1)在Redis4.0版本中,Redis引入多线程异步处理一些耗时比较长的任务,例如说异步删除命令unlink,对于一些BigKey如果直接进行删除的话,可能会导致主线程阻塞,那么就会耗时非常的久,这是往往会采取一种一部删除的策略,先标记成删除状态,后台再慢慢开启一个线程删除;

2)再Redis6.0版本中,在核心网络模型中引入了多线程进一步提高了对于多核CPU的利用率

3)redis是单线程的原因在于redis用单个CPU绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的

 Redis为什么要选择单线程?

对于Redis来说主要的性能瓶颈是内存和网络带宽而不是CPU

1)抛开持久化来说,Redis是纯内存操作,执行速度非常快,它的性能是网络延迟而不是执行速度,因此多线程并不能带来比较大的性能提升;

2)多线程会导致频繁的上下文切换,带来比不要的开销;

3)引入多线程会带来线程安全问题,必然要引入多线程锁这样的安全手段,实现起来复杂度高,而且性能也会大大下降;

4)数据结构简单,Redis的数据结构都是专门来进行设计的,而这些简单数据结构的查找和操作的时间复杂度都是O(1),所以性能比较高

5)多路复用和非阻塞IO:Redis使用IO多路复用功能来监听多个客户端Socket

就是客户端1删除这个大key的时候,主线程会阻塞,那么此时例如说有其他客户但想要连接主线程进行操作,此时就会发生阻塞,等到客户端1执行完成大key的删除,才会执行其他客户端的命令

 

一)服务器和客户端建立Socket连接,并分配处理线程:

首先主线程要负责接受连接请求,当有客户端请求和实例建立Socket链接的时候,主线程会创建和客户端Socket之间的连接,并把Socket存放到全局的等待队列中,主线程通过轮询的方式将Socket分配给IO线程;

二)IO线程会读取并解析请求

主线程一旦把Socket分配给IO线程,就会进入到阻塞模式,等待IO线程完成客户端的读取请求和解析,因为有多个IO线程在并行处理,因此这个该过程很快就会完成;

三)主线程完成请求操作

等到IO线程解析完成请求,主线程还是会以单线程的方式来执行命令操作(set k1 v1)

四)IO线程将结果写回到Socket和主线程清空全局队列,等待客户端后续请求

当主线程完成请求操作以后,会将返回的结果写回到client缓冲区,然后主线程会阻塞等待IO线程,IO线程会分别将客户端缓冲区中的数据写会到对应的Socket里面;

Redis的网络模型: 

Redis通过IO多路复用来提升网络性能,并且支持不同类型的多路复用实现,并且将这些实现进行了封装,提供了统一的高性能事件库AE;

 

1)首先在main函数里面启动初始化服务,调用initserver()函数,首先会调用aeCreateEventLoop函数,在这个函数里面会调用awApiCreate(eventLoop),eventLoop就是epoll实例,在操作系统内核里面红黑树和链表都已经准备好了;

2)接下来会调用listenToPort方法,指定监听TCP端口(6379)和Ip地址(云服务器地址),创建ServerSocket并得到了ServerSocket对应的FD,IP地址和端口号都是配置文件里面指定的;

3)接下来redis会继续调用createSocketAcceptHandler函数注册连接处理器,内部会调用aeApiAddEvent(&server,ipfd)来将ServerSocket对应的fd挂到红黑树上面并监听ServerSocket对应的FD,相当于是epoll_ctl函数了;

4)不同的事件有不同的处理器,因为监听ServerSocket要等待fd就绪,就意味着有客户端Socket连接上来了,就需要接受accept客户端的Socket,Socket接受的处理器就是AcceptTcpHandlerTCPAcceptHandler,连接应答处理器,这个处理器的作用就是处理SeverSocket上面的事件的,这个处理器至少要做两件事情:

3.1)监听ServerSocket;

3.2)一旦ServerSocket发生事件,及时做处理;

3.3)进行预测这个处理器做的工作应该是一旦ServerSocketssfd就绪了(现在还需要等待),那么直接处理器调用accept函数和客户端Socket进行建立连接,把这个客户端对应的FD实例注册到epoll实例上,也就是将这个客户端对应的fd加入到红黑树里面;

当有客户端连接的时候,TCPSocketHandler就开始干活了

5)最后调用了一个aeSetBeforeSleepProc函数,按照道理来讲,在监听已经注册好了以后,下一步就需要调用这个wait即可,一旦没有fd就绪,那么主线程就会直接休眠等待fd就绪,一旦fd就绪,内核就会被唤醒,这个函数也是在调用epoll_wait之前的一些准备工作;

6)调用aeapiPoll的函数的返回值就是fd就绪的数量,接着使用for循环遍历nummevents来处理对应的fd,调用对应fd里面的回调函数,当serverSocket中的fd就绪之后,这个TCPAcceptTcpHandler就需要接收客户端的请求,建立连接,得到客户端的Socket和这个客户端对应的fd,然后TCPAcceptHandler会把客户端对应的fd挂到红黑树上面,开始进行监听,这样红黑树里面监听的fd也会越来越多

7)connSetReadHandler也是将fd挂到红黑树上面,有创建了一个处理器readQueryFromClient这个命令请求处理器,这个读处理器又是在做两件事:

7.1)监听客户端socket的fd

7.2)一旦客户端Socket发生了事件,就是来进行处理

readQueryFromClient这个命令请求处理器的源码:

 

1)首先会给每一个客户端封装成一个对象Client对象,Client对象里面有queryBuffer查询缓冲区,接下来readQueryFromClient会把请求命令读取出来,存放到queryBuffer缓冲区里面

2)接下来又会调用processCommand函数来解析缓冲区的命令存放到argv的数组里面

最后调用processCommand函数来处理c->argv中的命令,其中的lookupCommand是根据命令的名称来查询到命令处理的函数,比如说是set命令,对应的函数就是setCommand,3)接下来调用c->cmd->proc是在调用这个Command命令函数,执行完函数之后会有一个响应结果,如果是执行set命令,最终会返回OK,如果是ping命令,最终会返回pong

4)最终调用addReply将结果返回给客户端,首先会尝试把结果写入到c-buf缓冲区里面,如果缓冲区写不下写入失败,就写入到c->reply链表里面,最后调用listAddNodeHead将客户端写到server.clients_pending_write这个队列里面,是redis服务器里面固定好的一个队列,因为随着请求越来越多,待写出的客户端也是越来越多,这些客户端都会排在这个队列里面,但是还没有写入到客户端Socket里面

1)首先会调用connGetPrivateData来进行获取到当前客户端的连接,每一个客户端Socket只要和Redis服务器建立了连接,Redis就会把这个客户端封装成一个Client实例,Client对象就包含了客户端的所有信息,甚至是客户端的请求信息;

2)接下来会读取客户端的请求数据(set name zhangsan)存放到querybuffer缓冲区里面,是以字节的形式存放的,querybuffer是用来缓存客户端的请求参数的,接下来需要调用processCommand函数解析缓冲区的字节,解析成字符串,最后以空格的形式给分隔开,解析成一个个的SDS对象,存放到argv数组中,最终调用processCommand函数来处理这个命令;

beforeSleep的源码:

SetXXXHandlerWithBarrier这样的函数在底层就是做了两件事,第一件事就是监听对应的FD,第二件事就是设置这个FD对应的处理器

1)在setAcceptHandler的时候,那么它监听的就是ServerSocket对应的fd,设置serverSocket对应的处理器TcpAcceptHandler

2)在SetReadQueryFromClientHandler的时候,监听的是客户端Socket对应的fd,然后又绑定了ReadQueryFromClient处理器,他所监听的是客户端Socket向ServerSocket发送的信息有请求来了,也就是读事件;

3)commSetWriteHandlerWithBarrier,这里面监听的是写事件,也就是向客户端Socket写入响应结果,在server.clients_pending_write还有很多Client对象呢,要把Client缓冲区中的数据写回到客户端Socket里面,这个方法就是遍历队列中的所有Client,监听fd写事件,绑定写处理器,sendReplyToClient命令回复处理器,从队列中取出对应的client对象,从client对象中取出缓冲区中的数据,这样就可以把ServerSocket处理的响应数据写回到客户端Socket;

1)以上是一个单线程模型,在Redis6中引入了多线程来提高IO的读写效率,因此在解析客户端命令,写响应结果的时候采取了多线程的方案,但是命令的核心执行IO多路服用模块仍然是主线程在执行;

2)在命令请求处理器中,要从客户端连接里面读取Socket,读出对应的请求,读取成功之后还需要解析请求querybuffer缓冲区里,然后再从缓冲区中读取数据解析转化成redis的命令,选择并执行命令最终将结果写入到client中,这个过程牵扯到了IO的读和写,读的速度受到了网络,带宽的影响,所以速度会比较低;

3)最后出发命令回复处理器,这里面又会触发网络IO的操作,因为要把client对象中的reply的数据写入到Socket里面,又会受到网络带宽和网络延时的影响;

总结:在传统的单线程模型下,真正影响性能的并不是命令的处理以及IO多路复用的事件监听,影响性能就是针对于网络读写

一)命令解析:假设此时客户端可读,在高并发的场景下,可能有多个客户端向Redis服务器发送数据,这个时候读事件非常多,主线程会通过轮询的方式将多个客户端交给不同的线程,由他们去并行的解析不同的客户端的数据,解析对应的命令,找到对应的命令;但是真正执行命令的时候,还是由主线程单线程执行命令的,串行执行,线程安全的,然后再将结果放到对应的缓冲区中;

二)向Socket中返回数据:一旦触发了写事件,要将client中的数据写回到Socket里面,有会涉及到网卡读写,开启多线程,来写;

总结:多线程可以提高Redis客户端处理的速度,减少因为网络IO的操作导致性能下降,但是命令处理的速度不变

Redis通信协议:

Redis是一个客户端服务器的软件,通信一般分成两步,不包括pipeline和pubsub

1)客户端向服务器发送一条命令

2)服务器解析并执行命令,返回响应结果给客户端

因此客户端发送命令的格式和服务器返回结果的格式必须有一个规范这个规范就是通信协议

在RESP中,通过首字节的字符来区分不同的数据类型,常见的数据类型包括5种:

1)单行字符串:首字节是"+",后面再跟上单行字符串,以CRLF("\r\n")结尾,是结束标识,例如返回OK,"+OK\r\n",通常应用于服务器返回简单字符串的场景;

2)错误:首字节是"-",和单行字符串相同,只是字符串的异常信息,例如:

"-Error Message\r\n";

3)数值类型:首字节是":"后面跟上数字格式的字符串,还是以CRLF结尾,例如":10\r\n"

4)多行字符串:首字节是"$",表示二进制安全的字符串,最多支持512MB

5)数组:里面是可以存储多个值的,首字节是"*",*后面跟上数组的元素个数,再跟上元素,元素数据类型不限

 

public class TcpSocket {
    public static void main(String[] args) throws IOException {
        //1.建立连接
        Socket clientSocket=new Socket("124.71.136.248",6379);
        //2.获取输入流输出流
        OutputStream outputStream=clientSocket.getOutputStream();
        PrintWriter writer=new PrintWriter(outputStream);
        InputStream inputStream= clientSocket.getInputStream();
        BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
        //3.发送数据
        SendStringRequest(writer);
        //4.读取首字节,判断数据类型的标识
        System.out.println(ReadStringResponse(reader));
    }

    private static Object ReadStringResponse(BufferedReader reader) throws IOException {
        //1.首先读取首字节
        int prefix= reader.read();
        //2.判断数据类型的标识
        switch(prefix){
            case '+'://单行字符串,直接读取一行即可
                return reader.readLine();
            case '-'://是异常,直接读取信息
                throw new RuntimeException(reader.readLine());
            case ':'://返回的是数字
                String line=reader.readLine();
                return String.valueOf(Long.valueOf(line));
            case '$'://是多行字符串,先读取长度,在读取数据
                int len= Integer.parseInt(reader.readLine());
                if(len==0){
                    return "";
                }
                if(len==-1){
                    return null;
                }
              //再读N个字节,假设没有特殊字符,那么直接读一行即可
                return reader.readLine();
            case '*':
                return ReadBufferArgvs(reader);
            default:
                throw new RuntimeException("未知数据");
        }
    }
    private static Object ReadBufferArgvs(BufferedReader reader) throws IOException {
        //1.首先获取数组的大小
        int len=Integer.parseInt(reader.readLine());
        if(len<=0){
            return null;
        }
        //2.遍历,依次读取每一个元素
        List<String> result=new ArrayList<>();
        for(int i=0;i<len;i++){
            result.add((String) ReadStringResponse(reader));
        }
      return result;
    }
    private static void SendStringRequest(PrintWriter writer) {
        String prefix="*2\r\n$4\r\nauth\r\n$8\r\n12503487\r\n";
        String request="*3\r\n$3\r\nset\r\n$4\r\nname\r\n$8\r\nshabiwan\r\n";
        writer.print(prefix+request);
        writer.flush();
    }
}

IO的读和写本身是阻塞的,比如说当Socket中有数据的时候,Redis就会通过系统调用先将数据从内核空间拷贝到用户空间,再交给Redis进行处理,这个拷贝的过程本身就是阻塞的,但数据量越大拷贝所需要的时间就越多,而这些操作都是基于单线程进行操作的;

1)假设现在A,B,C,D,E,F客户端都来向redis服务器建立连接,IO多路复用就是Redis服务器监听有哪一个Socket发送数据给服务器了Redis服务器就去处理对应的客户端的请求

2)从Redis6开始,就新增了多线程的功能来提升IO读写的效率,它的主要实现是将主线程的IO读写任务拆分给一组独立的线程去执行,这样就可以使多个Socket的读写并行化了,采用IO多路复用技术可以使单个线程高效的处理多个连接请求,尽量减少网络IO的时间消耗,将最耗时的Socket的读取,请求解析,写入操作外包出去,剩下的命令执行仍然由主线程串行执行和内存的数据交互

Redis在6.0支持的多线程并不是指令操作的多线程,而是针对于网络IO的多线程,在Redis的命令操作里面,仍然是线程安全的,其实Redis的性能瓶颈,主要取决于三个维度,网络,CPU和内存,而真正影响性能的关键问题是内存和网络,而Redis6.0的多线程主要是为了解决网络IO处理速度过慢的问题,Redis Server端在进行接收客户端的请求的时候,Socket连接的建立和指定的数据读取,解析,执行,写回都是通过一个线程来处理的,当客户端的请求比较多的时候,单个线程的网络请求处理速度太慢,导致各个客户端的请求处理效率非常的低,所以说要通过多个线程并发的方式来提升网络IO的读取效率,但是对于客户端的指定的执行过程,还是采用单线程的方式执行

 

 

 

 

 


http://www.niftyadmin.cn/n/393889.html

相关文章

Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配

Spring-Cloud-Gateway 整合 Sa-Token 全局过滤器之路由匹配 Sa-Token 是一个轻量级 Java 权限认证框架&#xff0c;主要解决&#xff1a;登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。 Sa-Token 旨在以简单、优雅的方式完…

如何通过控制点或地物点生产地方坐标系的倾斜摄影三维模型数据?

如何通过控制点或地物点生产地方坐标系的倾斜摄影三维模型数据&#xff1f; 要生成地方坐标系的倾斜摄影三维模型数据&#xff0c;需要进行以下步骤&#xff1a; 1、收集影像数据 首先需要采集大量的航空影像和地面影像&#xff0c;以构建真实世界中的物体模型。这些影像可以…

hdfs客户端定时日志采集任务的开发

idea已经新建好了项目,也添加了依赖项,然后要如何用代码来完成这个项目,求解答 工程开发1:新建子包hdfsClient,完成hdfs客户端定时日志采集任务的开发; 任务a:新建LogsTimingCollections类,实现定时日志采集任务及其调度; 任务b:新建LogsTimingCollectionsTask类,实…

Golang每日一练(leetDay0086) 回文链表、删除链表节点

目录 234. 回文链表 Palindrome Linked-list &#x1f31f; 237. 删除链表中的节点 Delete Node In a Linked-list &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练…

Java数据驱动:CData JDBC Drivers 2022 Crack

JDBC 驱动程序 易于使用的 JDBC 驱动程序&#xff0c;具有强大的企业级功能 无与伦比的性能和可扩展性。 对实时数据的简单 JDBC/SQL 访问。 从流行的 BI 工具访问实时数据。 集成到流行的 IDE 中。 CData JDBC Drivers Software 是领先的数据访问和连接解决方​​案提供商。我…

UART串口通信实验

不管是单片机开发还是嵌入式 Linux 开发&#xff0c;串口都是最常用到的外设。 可以通过串口将开发板与电脑相连&#xff0c;然后在电脑上通过串口调试助手来调试程序。 还有很多模块&#xff0c;比如蓝牙、GPS、GPRS等都使用串口与主控进行通信。 UART简介 串口全称串行接口…

【ARMv8 SIMD和浮点指令编程】NEON 减法指令——减法也好几种

向量减法包括常见的普通加指令&#xff0c;还包括长减、宽减、半减、饱和减、按对减、按对加并累加、选择高半部分结果加、全部元素加等。 1 SUB 减法&#xff08;向量&#xff09;&#xff0c;该指令从第一个源 SIMD&FP 寄存器中的相应向量元素中减去第二个源 SIMD&…

什么是循环语句?如何使用for循环、while循环和do-while循环?

1. 引言&#xff1a; 循环语句是一种编程结构&#xff0c;用于重复执行一段代码块&#xff0c;直到满足特定条件为止。它在程序中起到了简化代码、提高效率和处理大量数据的重要作用。在本文中&#xff0c;我们将详细讨论三种常见的循环语句&#xff1a;for循环、while循环和do…