Redis 单线程与多线程模型详解

发布于:2023-02-06 ⋅ 阅读:(851) ⋅ 点赞:(0)

大家好 我是积极向上的湘锅锅💪💪💪

IO模型 阻塞IO,非阻塞IO,IO多路复用,信号驱动IO,异步IO详解

1. API

Redis也通过IO多路复用提高性能,并且支持了各种不同的复用实现,在上节中select,poll,epoll模式中,是在linux的默认实现方案,但是不同的操作系统实现方案还有所不同,主要是以下几种:

在这里插入图片描述
可以发现都是ae开头,后面四个都是不同的实现方案,根据ae.c中的源码可以发现不同的操作系统选择的方案也不同

/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

可以看做是一个接口的不同重载实现,也可以看作根据不同方案选择代码块
以ae.select源码为例,值得关注的方法如下

在这里插入图片描述
不管是epoll_create还是epoll_ctl,想必不陌生,在上节已经讲的很详细
有了对应的方法了,接下来就开始走流程(关键)


2. 基于epoll的服务端流程

这个可以做为redis线程模型的前菜,因为这个流程跟redis的很像,也算是各种实现方案的老前辈了,把这个理解透再去看redis的就要轻松很多,不废话直接上图

在这里插入图片描述

根据箭头

  • 首先epoll_create在我们内核缓冲区创建实例,一个是红黑树rb_root,用来监听记录的FD,一个是链表list_head,用来记录就绪的FD

  • 创建serverSocket文件,得到FD,然后调用epoll_ctl去注册到rb_root红黑树中,同时使用回调函数,一旦FD就绪,就会记录到list_head链表里

  • 最后根据epoll_wait函数来等待数据就绪,首先判断链表是否为空,不为空就将对应链表的FD拷贝进空数组里面,然后让用户空间去遍历数组就可得到就绪的FD

  • 为空就一直等待,直到有FD就绪或者超时,如果FD超时还未就绪,只能返回一个0,告诉用户进程没有就绪,然后重新调用epoll_wait,直到FD就绪

这里的一个基本流程就是epoll的流程,不过FD已经是对应serverSocket,实际是对应的那个端口号创建的,默认是6379

那什么情况serverSocket才能就绪?
有服务端端口等待连接,就有客户端请求连接,所以serverSocket只有一种情况才能就绪,那就是有客户端进行连接,事件的类型为读事件

接下来就需要根据返回的FD数组中的事件类型进行数据的处理

在这里插入图片描述
跟上图就区别的地方就是多了下半部分,所以只需要关注下半部分即可

从判断事件开始

如果事件类型是EPOLLIN

  • 并且FD是ssfd,也就是serverSocket,那就证明有客户端要准备连接上服务端了,就会调用accept()接受客户端的socket,得到对应的客户端FD,最后需要调用epoll_ctl把客户端FD注册到rb_root红黑树上进行监听,后面新的客户端连接也是如此
  • FD不是ssfd,那就说明是客户端FD可读,那就读取请求参数进行业务处理,最后写出响应

如果事件类型是EPOLLERR
也就是各种异常,那就直接写出响应

下图就更为清晰的阐述了用户空间和内核空间所做的一系列事情,更为直观,详细流程可以再根据这幅图再走一遍
在这里插入图片描述


3. Redis 线程模型

前菜已去,该来的总会来

Redis 内部使用文件事件处理器,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型

基本流程简介:

它采用 IO 多路复用机制同时监听多个socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理

文件事件处理器的结构包含4个部分:
①多个socket;
②IO多路复用程序;
③文件事件分派器;
④事件处理器(命令请求处理器,命令回复处理器,连接应答处理器等等)

以上的有个概念就好,怎么用的在下文

Redis 线程模型流程

  1. 一开始,肯定需要服务端开启一个连接(调用epoll_create创建实例)等待客户端连接操作,所以就需要一个对应的处理器来处理连接,那就是连接应答处理器,什么时候用? go on reading

注:图中的aeEventLoop相当于epoll_create
在这里插入图片描述

  1. 创建好实例之后,就需要进行监听(图中的aeApiPoll就是epoll_wait)

在这里插入图片描述

根据箭头

承接上图,在aeEventLoop之后,有个beforsleep,具体什么用看后文,然后再是调用aeApipoll(epoll_wait)进行监听
如果serversoket有读事件,也就是在图的前面那三个蓝色圈圈 client Socket 要开始连接了,那就用连接应答处理器将client Socket的FD注册到我们创建的实例当中(红黑树添加节点)

连接应答处理器:处理对象是serversoket,作用将客户端FD注册到实例当中

  1. 客户端FD注册之后,那就可能不只是服务端可读事件发生,而是可能就是客户端FD可读,那就需要命令请求处理器来处理

在这里插入图片描述
在我们的epoll网络模型中,这一步就应该是用户空间获取请求参数进行处理

那在redis线程模型中,命令请求处理器所做的事情大致分为以下几类

在这里插入图片描述

  • 每一个client都有属于自己的读缓冲区queryBuf,将请求数据写入client的的queryBuf缓冲区里面
  • 再调用函数将数据解析为redis命令
  • 执行redis命令
  • 将执行结果再次写入client写缓冲区buf里面,如果buf写不下,就会写到一个reply链表里面,理论上容量无上限
  • 将客户端添加到服务端定义好的队列里面去,这个队列就是存放那些被等待写出的客户端的,随着要处理的客户端越来越多,这个队列里面存放的待写出的客户端也就越来越多

到这一步,仅仅是处理了数据,但是还没有真正的写出
那什么时候写?
注意前文的 beforsleep

查看beforsleep源码

在这里插入图片描述
可以发现是遍历了队列里面的每一个client socket,监听FD的写事件,并且绑定了一个处理器,叫做 命令回复处理器

命令回复处理器:将准备好的客户端缓冲区的数据取出,然后逐个写入到client socket里面

在这里插入图片描述


总结

总的来说会监听服务端的读事件,同时也会监听客户端的读事件和写事件,分别由专门的处理器处理

一共由三种事件

  • 服务端可读(连接应答处理器)
  • 客户端可读(命令请求处理器)
  • 客户端可写(命令回复处理器)

根据其中的特点可以概括为IO多路复用+事件派发

Redis单线程模型的瓶颈在哪里?
redis执行命令也就是ms级别,而将数据放入缓存,也就是纯内存操作,也花不了太长时间,那真正可以使用多线程优化的地方时哪里?
就是网络IO,IO在很多地方都有体现,数据库需要IO从磁盘读取数据,而这里就是网络IO,主要体现在俩个地方

  • 在处理客户端socket的读事件时,要从客户端socket读取数据并且解析为命令,这个操作涉及网络IO
  • 当从队列取出数据的时候,又要涉及到从服务端socket写数据到客户端socket,也涉及网络IO

因此,在Redis 6.0 版本中引入了多线程,目的就是为了提高IO读写效率,因此在解析客户端命令,写响应结果时采用了多线程,而核心命令的执行,IO多路复用的模块还是单线程

整个Redis线程模型就是如下过程

在这里插入图片描述

整个流程就结束了,建议拿自己慢慢画出来,熟练掌握之后,也就可以去跟面试官吹逼了


网站公告

今日签到

点亮在社区的每一天
去签到