文章目录[隐藏]
Redis网络模型
1. 用户空间和内核空间
服务器大多都采用 Linux 系统,这里我们以 Linux 为例来讲解:
ubuntu 和 Centos 都是 Linux 的发行版,发行版可以看成对 Linux 包了一层壳,任何 Linux 发行版,其系统内核都是 Linux。我们的应用都需要通过 Linux 内核与硬件交互
用户的应用,比如 redis,mysql 等其实是没有办法去执行访问我们操作系统的硬件的,所以我们可以通过发行版的这个壳子去访问内核,再通过内核去访问计算机硬件
计算机硬件包括,如 cpu,内存,网卡等等,内核(通过寻址空间)可以操作硬件的,但是内核需要不同设备的驱动,有了这些驱动之后,内核就可以去对计算机硬件去进行 内存管理,文件系统的管理,进程的管理等等
我们想要用户的应用来访问,计算机就必须要通过对外暴露的一些接口,才能访问到,从而简洁的实现对内核的操控,但是内核本身上来说也是一个应用,所以他本身也需要一些内存,cpu 等设备资源,用户应用本身也在消耗这些资源,如果不加任何限制,用户去操作随意的去操作我们的资源,就有可能导致一些冲突,甚至有可能导致我们的系统出现无法运行的问题,因此我们需要把用户和内核隔离开
进程的寻址空间划分成两部分:内核空间、用户空间
什么是寻址空间呢?我们的应用程序也好,还是内核空间也好,都是没有办法直接去物理内存的,而是通过分配一些虚拟内存映射到物理内存中,我们的内核和应用程序去访问虚拟内存的时候,就需要一个虚拟地址,这个地址是一个无符号的整数,比如一个 32 位的操作系统,他的带宽就是 32,他的虚拟地址就是 2 的 32 次方,也就是说他寻址的范围就是 0~2 的 32 次方, 这片寻址空间对应的就是 2 的 32 个字节,就是 4GB,这个 4GB,会有 3 个 GB 分给用户空间,会有 1GB 给内核系统
在 Linux 中,他们权限分成两个等级,0 和 3,用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问内核空间可以执行特权命令(Ring0),调用一切系统资源,所以一般情况下,用户的操作是运行在用户空间,而内核运行的数据是在内核空间的,而有的情况下,一个应用程序需要去调用一些特权资源,去调用一些内核空间的操作,所以此时他俩需要在用户态和内核态之间进行切换。
比如:
Linux 系统为了提高 IO 效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
针对这个操作:我们的用户在写读数据时,会去向内核态申请,想要读取内核的数据,而内核数据要去等待驱动程序从硬件上读取数据,当从磁盘上加载到数据之后,内核会将数据写入到内核的缓冲区中,然后再将数据拷贝到用户态的 buffer 中,然后再返回给应用程序,整体而言,速度慢,就是这个原因,为了加速,我们希望 read 也好,还是 wait for data 也最好都不要等待,或者时间尽量的短
2. 阻塞 IO
在《UNIX 网络编程》一书中,总结归纳了 5 种 IO 模型:
- 阻塞 IO(Blocking IO)
- 非阻塞 IO(Nonblocking IO)
- IO 多路复用(IO Multiplexing)
- 信号驱动 IO(Signal Driven IO)
- 异步 IO(Asynchronous IO)
应用程序想要去读取数据,他是无法直接去读取磁盘数据的,他需要先到内核里边去等待内核操作硬件拿到数据,这个过程就是 1,是需要等待的, 等到内核从磁盘上把数据加载出来之后,再把这个数据写给用户的缓存区,这个过程是 2,如果是阻塞 IO,那么整个过程中, 用户从发起读请求开始,一直到读取到数据,都是一个阻塞状态
具体流程如下图:
用户去读取数据时,会去先发起 recvform 一个命令,去尝试从内核上加载数据, 如果内核没有数据,那么用户就会等待,此时内核会去从硬件上读取数据,内核读取数据之后,会把数据拷贝到用户态,并且返回 ok,整个过程,都是阻塞等待的,这就是阻塞 IO
总结如下:
顾名思义,阻塞 IO 就是两个阶段都必须阻塞等待:
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 此时用户进程也处于阻塞状态
阶段二:
- 数据到达并拷贝到内核缓冲区,代表已就绪
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
可以看到,阻塞 IO 模型中,用户进程在两个阶段都是阻塞状态
3. 非阻塞 IO
顾名思义,非阻塞 IO 的 recvfrom 操作会立即返回结果而不是阻塞用户进程
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 返回异常给用户进程
- 用户进程拿到 error 后,再次尝试读取
- 循环往复,直到数据就绪
阶段二:
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
可以看到,非阻塞 IO 模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致 CPU 空转, CPU 使用率暴增
4. IO 多路复用
无论是阻塞 IO 还是非阻塞 IO,用户应用在一阶段都需要调用 recvfrom 来获取数据,差别在于无数据时的处理方案:
- 如果调用 recvfrom 时,恰好没有数据,阻塞 IO 会使 CPU 阻塞,非阻塞 IO 使 CPU 空转,都不能充分发挥 CPU 的作用
- 如果调用 recvfrom 时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
所以怎么看起来以上两种方式性能都不好
而在单线程情况下,只能依次处理 IO 事件,如果正在处理的 IO 事件恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有 IO 事件都必须等待,性能自然会很差
就比如服务员给顾客点餐,分两步:
- 顾客思考要吃什么(等待数据就绪)
- 顾客想好了,开始点餐(读取数据)
要提高效率有几种办法?
- 方案一:增加更多服务员(多线程)
- 方案二:不排队,谁想好了吃什么(数据就绪了),服务员就给谁点餐(用户应用就去读取数据)
那么问题来了:用户进程如何知道内核中数据是否就绪呢?
所以接下来就需要详细的来解决多路复用模型是如何知道到底怎么知道内核数据是否就绪的问题了
这个问题的解决依赖于提出的
文件描述符(File Descriptor):简称 FD, 是一个从 0 开始的无符号整数,用来关联 Linux 中的一个文件。在 Linux 中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)
IO 多路复用:是利用单个线程来同时监听多个 FD,并在某个 FD 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源
阶段一:
- 用户进程调用 select, 指定要监听的 FD 集合
- 内核监听 FD 对应的多个 socket
- 任意一个或多个 socket 数据就绪则返回 readable
- 此过程中用户进程阻塞
阶段二:
- 用户进程找到就绪的 socket
- 依次调用 recvfrom 读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据
当用户去读取数据的时候,不再去直接调用 recvfrom 了,而是调用 select 的函数,select 函数会将需要监听的数据交给内核,由内核去检查这些数据是否就绪了,如果说这个数据就绪了,就会通知应用程序数据就绪,然后来读取数据,再从内核中把数据拷贝给用户态,完成数据处理,如果 N 多个 FD 一个都没处理完,此时就进行等待
用 IO 复用模式,可以确保去读数据 (recvfrom) 的时候,数据是一定存在的,他的效率比原来的阻塞 IO 和非阻塞 IO 性能都要高
IO 多路复用是利用单个线程来同时监听多个 FD,并在某个 FD 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源。不过监听 FD 的方式、通知的方式又有多种实现,常见的有:
- select
- poll
- epoll
select 和 poll 只会通知用户进程有 FD 就绪,但不确定具体是哪个 FD,需要用户进程逐个遍历 FD 来确认
epoll 则会在通知用户进程 FD 就绪的同时,把已就绪的 FD 写入用户空间
4.1 select 模式
select 是 Linux 最早是由的 I/O 多路复用技术:
简单说,就是我们把需要处理的数据封装成 FD,然后在用户态时创建一个 fd 的集合(这个集合的大小是要监听的那个 FD 的最大值 + 1,但是大小整体是有限制的 ),这个集合的长度大小是有限制的,同时在这个集合中,标明出来我们要控制哪些数据
nfds
:fd_set 中最大 fd+1,在此例中是 6,遍历时遍历到 nfds 时就可以不再遍历
__fd_mask fds_bits [__FD_SETSIZE /__NFDBISTS]
:存储要监听的 fd 的数组,是一个长度为 32 的 long int 数组,但是存储一个 fd 只需要一个 bit,而 long int 是 4 个字节,所以可以存储 32 (4 8) = 1024 个 fd
比如要监听的数据,是 1,2,5 三个数据,二进制位为 00010011 ,步骤如下:
- 此时用户进程会执行 select 函数,然后将整个 fd 拷贝给内核态,内核态会去遍历用户态传递过来的数据,如果发现这里边都数据都没有就绪,就休眠
- 直到有数据准备好时,就会被唤醒,唤醒之后,再次遍历一遍,看看谁准备好了,然后再将处理掉没有准备好的数据(设为 0)
- 最后再将这个 FD 集合写回到用户态中去,覆盖之前的用户 fd 集合
- 因为用户进程并不知道哪个是准备好的 fd,因此还需要再遍历一遍 fd 集合,找到对应准备好数据的节点,再去发起读请求
select 模式存在的问题:
- 需要将整个 fd_set 从用户空间拷贝到内核空间,select 结束还要再次拷贝回用户空间
- select 无法得知具体是哪个 fd 就绪,需要遍历整个 fd_set
- fd_set 监听的 fd 数量不能超过 1024
4.2 poll 模式
poll 模式对 select 模式做了简单改进,但性能提升不明显,部分关键代码如下:
IO 流程:
- 创建 pollfd 数组,向其中添加关注的 fd 信息,数组大小自定义
- 调用 poll 函数,将 pollfd 数组拷贝到内核空间,转链表存储,无上限
- 内核遍历 fd,判断是否就绪
- 数据就绪或超时后,拷贝 pollfd 数组到用户空间,返回就绪 fd 数量 n
- 用户进程判断 n 是否大于 0, 大于 0 则遍历 pollfd 数组,找到就绪的 fd
与 select 对比:
- select 模式中的 fd_set 大小固定为 1024,而 pollfd 在内核中采用链表,理论上无上限
- 虽然 pollfd 没有上限,但是监听 FD 越多,每次遍历消耗时间也越久,性能反而会下降
- 仍然是要拷贝两次
4.3 epoll 模式
epoll 模式是对 select 和 poll 的改进,它提供了三个函数:
第一个是:创建 eventpoll 的函数 epoll_create,eventpoll 内部包含两个东西:
- 1、红黑树 -> 记录的是要监听的 FD
- 2、链表 -> 一个链表,记录的是就绪的 FD
第二个是:epoll_ctl 操作:将一个要监听的数据添加到红黑树上去,并且给每个 fd 设置一个监听函数,这个函数会在 fd 数据就绪时触发,当 fd 准备好了,就把 fd 添加到 list_head 中去
第三个是:调用 epoll_wait 函数:就去等待,在用户态创建一个空的 events 数组,当就绪之后,我们的回调函数会把就绪的 fd 添加到 list_head 中去,当调用这个函数的时候,会去检查 list_head,当然这个过程需要参考配置的等待时间,可以等一定时间,也可以一直等,如果在此过程中,检查到了 list_head 中有数据会将数据添加到链表中,此时将数据放入到 events 数组中,并且返回对应的操作的数量,用户态的此时收到响应后,从 events 中拿到对应准备好的数据的节点,再去调用方法去拿数据
小总结:
select 模式存在的三个问题:
- 能监听的 FD 最大不超过 1024
- 每次 select 都需要把所有要监听的 FD 都拷贝到内核空间
- 每次都要遍历所有 FD 来判断就绪状态
poll 模式的问题:(只解决了 select 的第一个问题)
- poll 利用链表解决了 select 中监听 FD 上限的问题,但依然要遍历所有 FD,如果监听较多,性能会下降
epoll 模式中如何解决这些问题的?
- 基于 epoll 实例中的红黑树保存要监听的 FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的 FD 数量增多而下降
- 每个 FD 只需要执行一次 epoll_ctl 添加到红黑树,以后每次 epol_wait 无需传递任何参数,无需重复拷贝 FD 到内核空间
- 利用 ep_poll_callback 机制来监听 FD 状态,内核只将就绪的 FD 拷贝到用户空间的指定位置,无需遍历所有 FD
4.3.1 事件通知机制- LT 和 ET
当 FD 有数据可读时,我们调用 epoll_wait(或者 select、poll)可以得到通知。但是事件通知的模式有两种:
- LevelTriggered:简称 LT,也叫做 水平触发。当 FD 有数据可读时,会重复通知多次,直至数据处理完成。是 Epol 的默认模式。
只要某个 FD 中有数据可读,每次调用 epoll_wait 都会得到通知 - EdgeTriggered:简称 ET,也叫做 边沿触发。当 FD 有数据可读时,只会被通知一次,不管数据是否处理完成。
只有在某个 FD 有状态变化时,调用 epoll_wait 才会被通知
举个例子:
- 假设一个客户端 socket 对应的 FD 已经注册到了 epoll 实例中
- 客户端 socket 发送了 2kb 的数据
- 服务端调用 epoll_wait,得到通知说 FD 就绪
- 服务端从 FD 读取了 1kb 数据
- LT 模式会回到步骤 3(再次调用 epoll_wait,形成循环),而 ET 模式不会
如果我们采用 LT 模式,因为 FD 中仍有 1kb 数据,则第五步依然会返回结果,并且得到通知
如果我们采用 ET 模式,因为第三步已经消费了 FD 可读事件,第五步 FD 状态没有变化,因此 epoll_wait 不会返回,数据无法读取,客户端响应超时
每当调用 epoll_wait 读取走数据后,系统会判断当前是 LT 模式还是 ET 模式:
- 若是 LT 模式,则不会断开 list_head 中与就绪数据的链接,下次读取时仍然能够发现有就绪数据
- 若是 ET 模式,则会断开 list_head 中与就绪数据的链接,下次读取时将没有数据可读
结论:
- ET 模式避免了 LT 模式可能出现的惊群现象
- ET 模式最好结合非阻塞 IO 读取 FD 数据(利用代码实现,在一次读取数据时将所有数据消费完,或者手动调用 epoll_ctl 函数,重新连接 list_head 和就绪数据),相比 LT 会复杂一些
4.3.2 基于 epoll 的 Web 服务流程
服务器启动以后,服务端会去调用 epoll_create,创建一个 epoll 实例,epoll 实例中包含两个数据
1、红黑树(为空):rb_root 用来去记录需要被监听的 FD
2、链表(为空):list_head,用来存放已经就绪的 FD
创建好了之后,会去调用 epoll_ctl 函数,此函数会将需要监听的数据添加到 rb_root 中去,并且对当前这些存在于红黑树的节点设置回调函数,当这些被监听的数据一旦准备完成,就会被调用,而调用的结果就是将红黑树的 fd 添加到 list_head 中去 (但是此时并没有完成)
3、当第二步完成后,就会调用 epoll_wait 函数,这个函数会去校验是否有数据准备完毕(因为数据一旦准备就绪,就会被回调函数添加到 list_head 中),在等待了一段时间后 (可以进行配置),如果等够了超时时间,则返回没有数据,如果有,则进一步判断当前是什么事件,如果是建立连接事件,则调用 accept () 接受客户端 socket,拿到建立连接的 socket,然后建立起来连接, 如果是其他事件,则把数据进行写出
这里的 ssfd 是否可读有两种条件:
- 可读就是有客户端连接来了(ssfd 可读)
- 非可读就是 web 已有的客户端请求接口进行数据访问,包括接收客户端请求数据(客户端读事件)和回复响应数据(客户端写事件)
5. 信号驱动 IO
信号驱动 IO 是与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 就绪时,会发出 SIGIO 信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
阶段一:
- 用户进程调用 sigaction,注册信号处理函数
- 内核返回成功,开始监听 FD
- 用户进程不阻塞等待,可以执行其它业务
- 当内核数据就绪后,回调用户进程的 SIGIO 处理函数
阶段二:
- 收到 SIGIO 回调信号
- 调用 recvfrom,读取
- 内核将数据拷贝到用户空间
- 用户进程处理数据
缺点:
当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低
6. 异步 IO
这种方式,不仅仅是用户态在试图读取数据后,不阻塞,而且当内核的数据准备完成后,也不会阻塞
他会由内核将所有数据处理完成后,由内核将数据写入到用户态中,然后才算完成,所以性能极高,不会有任何阻塞,全部都由内核完成,可以看到,异步 IO 模型中,用户进程在两个阶段都是非阻塞状态
缺点:
性能 “太高”,因为 IO 读写效率较低,用户进程不阻塞的话,高并发场景下就会一直给内核下达任务,最终可能导致 OOM。所以,若要使用异步 IO,必须做好限流策略
7. 五种 IO 模型的对比
IO 操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的 IO 操作),也就是阶段二是同步还是异步:
前四种都是同步 IO,只有异步 IO 是异步的
使用最多的是 IO 多路复用,少数情况下会用到异步 IO 但要做好限流
8. Redis 线程
Redis 到底是单线程还是多线程?
- 如果仅仅聊 Redis 的核心业务部分(命令处理),答案是单线程
- 如果是聊整个 Redis,那么答案就是多线程
在 Redis 版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
- Redis v4.0:引入多线程异步处理一些耗时较长的任务,例如异步删除命令 unlink
- Redis v6.0:在核心网络模型中引入多线程,进一步提高对于多核 CPU 的利用率
因此,对于 Redis 的核心网络模型,在 Redis 6.0 之前确实都是单线程。是利用 epoll(Linux 系统)这样的 IO 多路复用技术在事件循环中不断处理客户端情况
为什么 Redis 要选择单线程?
- 抛开持久化不谈,Redis 是纯内存操作,执行速度非常快,它的性能瓶颈是网络带宽、IO 速度而不是执行速度,因此多线程并不会带来巨大的性能提升
- 多线程会导致过多的上下文切换,带来不必要的开销
- 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
Redis 通过 IO 多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装, 提供了统一的高性能事件库 API 库 AE:
看源码时结合下面的的流程图:
- 调用 aeCreateEventLoop 方法 (类似 epoll_create),创建实例
- 调用 listenToPort 方法,创建 serverSocket 得到 ssfd
- 调用 createSocketAcceptHandler 方法注册监听 ssfd,对 ssfd 绑定连接应答处理器 tcpAcceptHandler(类似服务端的epoll_ctl)
- tcpAcceptHandler 的触发条件是有新的客户端连接进来了,作用是接收客户端 socket 数据得到其中的 fd,然后创建连接关联客户端 fd,监听客户端 fd,并对客户端 fd 绑定读命令请求处理器 readQueryFromClient
- 注册 aeApiPoll 前置处理器 beforeSleep,调用 aeApiPoll 前调用
- 调用 aeApiPoll 前监听 server.clients_pending_write 队列,即监听队列中是否有第一次处理好请求的 client,若有则对 client 绑定写命令回复处理器 sendReplyToClient,可以把该 client 准备好的数据响应写到客户端。(一个 client 代表一次连接,但是一个连接可以有很多请求,即很多 fd)
- 注册监听好 ssfd 并绑定好客户端读 / 写命令处理器后,先调用 beforeSleep,再调用 aeApiPoll 方法 (类似 epoll_wait),开始循环监听事件,看是否有 fd 准备就绪(这里的 fd 包括服务端的 ssfd 和客户端 fd:)
- 服务端 ssfd 的作用是当有客户端请求进来时调用连接应答处理器 tcpAcceptHandler 对客户端 socket 进行连接,注册监听其中的 fd 并绑定 readQueryFromClient (类似客户端的 epoll_ctl)(对应 ssfd 可读)
- 客户端 fd 有两种(对应 ssfd 不可读),一种是读事件,发生在客户端 fd 刚注册绑定好过后 (即 IO 多路复用模型中第一阶段的系统调用),会调用读命令请求处理器 readQueryFromClient:先通过客户端连接对象 conn 得到客户端对象 client,将客户端连接 conn 中的请求数据写入该客户端对象 client 的 querybuffer,然后解析 querybuffer 中的数据转换为 Redis 命令存入 argv 数组中,然后客户端拿出 argv 数组中的命令选择执行 Redis 命令后得到执行结果,先将结果写入该客户端对象 client 的 buf (客户端写缓冲区) 中,如果写不下就写入客户端对象 client 的 reply 链表中 (容量无上限),将客户端添加到 server.clients_pending_write 这个队列,等待被写出 (自此,IO 多路复用模型中第一阶段的等待数据完成)
- 另一种是写事件,在第 4 步时因为每次调用 aeApiPoll 前都会调用 beforeSleep,而 beforeSleep 会遍历 server.clients_pending_write 队列检查是否有准备好数据的 client,并会对该队列中的每个 client 都绑定上了写命令回复处理器,当该 client 有数据准备好时即调用绑定的写命令回复处理器 sendReplyToClient,把准备好的响应数据写到客户端 socket 发送给客户端 (即 IO 多路复用模型中第二阶段返回数据给用户进程的过程)
参考一下大佬写的流程 (引自 Redis 多线程网络模型全面揭秘):
- Redis 服务器启动,开启主线程事件循环(Event Loop),注册
acceptTcpHandler
连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接到来;- 客户端和服务端建立网络连接;
acceptTcpHandler
被调用,主线程使用 AE 的 API 将readQueryFromClient
命令读取处理器绑定到新连接对应的文件描述符上,并初始化一个client
绑定这个客户端连接;- 客户端发送请求命令,触发读就绪事件,主线程调用
readQueryFromClient
通过 socket 读取客户端发送过来的命令存入client->querybuf
读入缓冲区;- 接着调用
processInputBuffer
,在其中使用processInlineBuffer
或者processMultibulkBuffer
根据 Redis 协议解析命令,最后调用processCommand
执行命令;- 根据请求命令的类型(SET, GET, DEL, EXEC 等),分配相应的命令执行器去执行,最后调用
addReply
函数族的一系列函数将响应数据写入到对应client
的写出缓冲区:client->buf
或者client->reply
,client->buf
是首选的写出缓冲区,固定大小 16KB,一般来说可以缓冲足够多的响应数据,但是如果客户端在时间窗口内需要响应的数据非常大,那么则会自动切换到client->reply
链表上去,使用链表理论上能够保存无限大的数据(受限于机器的物理内存),最后把client
添加进一个 LIFO 队列clients_pending_write
;- 在事件循环(Event Loop)中,主线程执行
beforeSleep
–>handleClientsWithPendingWrites
,遍历clients_pending_write
队列,调用writeToClient
把client
的写出缓冲区里的数据回写到客户端,如果写出缓冲区还有数据遗留,则注册sendReplyToClient
命令回复处理器到该连接的写就绪事件,等待客户端可写时在事件循环中再继续回写残余的响应数据
中间三个即 IO 多路复用 + 事件派发:
当我们的客户端想要去连接我们服务器,会去先到 IO 多路复用模型去进行排队,会有一个连接应答处理器,他会去接受读请求,然后又把读请求注册到具体模型中去,此时这些建立起来的连接,如果是客户端请求处理器去进行执行命令时,他会去把数据读取出来,然后把数据放入到 client 中, clinet 去解析当前的命令转化为 redis 认识的命令,接下来就开始处理这些命令,从 redis 中的 command 中找到这些命令,然后就真正的去操作对应的数据了,当数据操作完成后,会去找到命令回复处理器,再由他将数据写出
Redis6.0 引入多线程网络模型:
Rdis6.0 版本中引入了多线程,目的是为了提高引 IO 读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO 多路复用模块依然是由主线程执行
Redis 的瓶颈是带宽 IO 操作,所以多线程只用于网络模型中的 IO 操作:
如图:只在接收客户端命令时有 IO 操作,在回复响应数据时有 IO 操作,在这两个地方使用多线程,能够加快 IO 速度,提升 Redis 服务吞吐量