前言

Redis6.0引入了多线程模型,那么在Redis6.0之前,Redis是单线程模型,那么单线程模型的Redis的底层模型是什么,为什么单线程模型还能那么快,本篇文章将对Redis的单线程模型进行学习。

注:Redis是单线程仅指Redis服务端的网络IO是单线程的,Redis的集群数据同步,长久化等都是多线程的。

注释

一. 筹备常识

学习Redis中的单线程模型,会波及一些操作系统相干的常识,所以先对这些常识进行一个简略介绍。

1. linux操作系统中的I/O

linux操作系统中,所有事物对象都是文件,linux执行任何模式的I/O操作时,都是对一个文件描述符进行读取或写入,留神这里的文件描述符不单会关联传统意义上的文件,还可能会关联管道,键盘,显示器或者网络连接。

2. socket概念

既然linux操作系统中的任何模式的I/O都是对一个文件描述符的读取或写入,那么网络I/O也不例外,通过socket()函数能够创立网络连接,其返回的socket就是文件描述符,通过socket就能够像操作文件那样来操作网络通信,例如应用read()函数来读取对端计算机传来的数据,应用write()函数来向对端计算机发送数据。

socket又叫套接字,其将不同主机上的过程或者雷同主机上的不同过程进行通信做了一层形象,也能够将socket了解为应用层到传输层的一层形象。下表给出了两种罕用的socket

socket类型阐明
流式socket用于TCP通信,提供牢靠的,面向连贯的通信。
数据报socket用于UDP通信,提供不牢靠,无连贯的通信。

在进行网络通信的时候,须要一对socket,一个运行于客户端,一个运行于服务端,下图进行一个简略示意。

那么整个通信流程能够进行如下概括。

  • 服务端运行后,会在服务端创立listen-socketlisten-socket会绑定服务端的ipport,而后服务端进入监听状态;
  • 客户端申请服务端时,客户端创立connect-socketconnect-socket形容了其要连贯的服务端的listen-socket,而后connect-socketlisten-socket发动连贯申请;
  • connect-socketlisten-socket胜利连贯后(TCP三次握手胜利),服务端会为已连贯的客户端创立一个代表该客户端的client-socket,用于后续和客户端进行通信;
  • 客户端与服务端通过socket进行网络I/O操作,此时就实现了客户端和服务端中的不同过程的通信。

3. 用户过程缓冲区与内核缓冲区

用户过程拜访系统资源(磁盘,网卡,键盘等)时,须要切换到内核态(Kernel Mode),拜访完结后,又须要从内核态切换为用户态(User Mode),这种切换非常耗时,所以用户过程会在用户过程空间中开拓一块缓冲区域,叫做用户过程缓冲区,用户过程如果是读系统资源,则会将读到的系统资源写入用户过程缓冲区,后续读就读用户过程缓冲区的内容,用户过程如果是写数据到系统资源,则会将写的数据先写入用户过程缓冲区,而后再将用户过程缓冲区的内容写到系统资源。所以用户过程缓存区会缩小用户过程在用户态和内核态之间的切换次数,从而升高切换的工夫。

用户过程拜访系统资源实际上须要借助操作系统内核实现,所以与系统资源产生I/O的理论是操作系统内核,操作系统内核为了缩小与系统资源理论的I/O的次数,也有一个缓冲区叫做内核缓冲区,如果是对系统资源的读,则先将系统资源数据读取并写入内核缓冲区中,而后再将内核缓冲区的内容写入用户过程缓冲区,如果是对系统资源的写,则先将用户过程缓冲区的内容写入内核缓冲区, 而后再将内核缓冲区的内容写到系统资源。这样能够无效升高操作系统内核与系统资源的理论I/O次数,升高I/O带来的工夫耗费。

上面以一个服务端解决一个客户端申请为例,对用户过程缓冲区与内核缓冲区进行一个更直观的阐明。(注:理论的网络申请比上面的图示更为简单,上面的图示只是一个大抵流程的体现,目标是帮忙领会用户过程缓冲区与内核缓冲区的作用)

那么上述过程能够概括如下。

  • 操作系统内核通过与网卡(系统资源)进行网络I/O读取客户端申请数据到内核缓冲区中;
  • 服务端用户过程将内核缓冲区内容写入用户过程缓冲区中,随后用户过程读取用户过程缓冲区的内容并解决业务;
  • 服务端用户过程将响应内容写入用户过程缓冲区,而后再将用户过程缓冲区的内容写入内核缓冲区;
  • 操作系统内核通过与网卡进行网络I/O,将内核缓冲区的内容写到网卡。

4. 同步阻塞I/O,同步非阻塞I/OI/O多路复用

同步阻塞I/O是用户过程调用read时发动的I/O操作,此时用户过程由用户态转换到内核态,只有在内核态中将I/O操作执行完后,才会从内核态切换回用户态,这期间用户过程会始终阻塞。同步阻塞I/OBlocking IOBIO)示意图如下。

同步非阻塞I/O是用户过程调用read时,用户过程由用户态转换到内核态后,此时如果没有系统资源数据可能被读取到内核缓冲区中,返回read失败,并从内核态切换回用户态。也就是用户过程发动I/O操作后会立刻失去一个操作后果,同时用户过程须要在read失败时始终反复的发动read,直至read胜利。同步非阻塞I/ONon-Blocking IO,NIO)示意图如下。

I/O多路复用是一个用户过程中对多个文件描述符进行监控,一旦有文件描述符能够进行I/O操作,内核会告诉用户过程对相应的文件描述符进行I/O操作。最简略的实现是应用select操作来实现对多个文件描述符的监控,具体做法如下。

  • 在用户过程中将文件描述符注册到select的文件描述符列表中;
  • 执行select操作,此时用户过程由用户态转换到内核态,而后内核会查找出select的文件描述符列表中所有能够进行I/O操作的文件描述符,并返回,此时内核态转换到用户态;
  • 用户过程在select操作返回前会始终阻塞,直至select操作返回,此时用户过程取得了能够I/O的文件描述符列表;
  • 用户过程取得了能够I/O操作的文件描述符列表后,会对列表中每个文件描述符发动I/O操作。

I/O多路复用(IO Multiplexing)能够用下图进行示意。

5. 单Reactor单线程模型

有了下面1-4点的根底,当初来介绍单Reactor单线程模型。已知在客户端与服务端通信的过程中,呈现了三种socket,如下所示。

  • listen-socket,是服务端用于监听客户端建设连贯的socket
  • connect-socket,是客户端用于连贯服务端的socket
  • client-socket,是服务端监听到客户端连贯申请后,在服务端生成的与客户端连贯的socket

(注:上述中的socket,能够被称为套接字,也能够被称为文件描述符。)

那么先看一下如下的客户端申请服务端的模型。

上图中的Server主线程中创立了listen-socket用于监听客户端的连贯申请,当Client1创立connect1-socket并发动connect操作时,Server主线程会从accept操作返回并失去代表Client1client1-socket,随后Server在主线程中解决Client1的申请,此时Client2创立connect2-socket并发动connect操作,因为Server主线程正在解决Client1的申请,所以Server此时不会立刻与Client2建设连贯,等到Server主线程中解决完了Client1的申请并断开与Client1的连贯后,此时Server才会再与Client2建设连贯。上述的客户端申请服务端的模型,实质就是同步阻塞I/O模型,对于服务端来说,这种模型有两个问题,如下所示。

  • 服务端是单线程的,同一时间只能在服务端主线程中监听到一个客户端建设连贯的申请,并且只会在解决完以后建设了连贯的客户端的申请后,才会持续与下一个客户端建设连贯;
  • 服务端的listen-socketaccept操作是阻塞的,服务端与客户端建设连贯后,client-socketreadwrite操作是阻塞的,换句话说,服务端要么阻塞在listen-socketaccept操作上,要么阻塞在client-socketreadwrite操作上。

那么单Reactor单线程模型在引入了多路复用I/O后,对下面第二个问题进行了优化:服务端主线程中应用select或者epoll等操作,来同时监督listen-socketclient-socket。以select举例,服务端主线程中一开始会在select的文件描述符列表中增加listen-socket,随后调用select进入监督状态(此时主线程阻塞在select上),此时如果客户端的connect-socket发动了connect操作,服务端主线程就会从select上返回,并且判断是listen-socket准备就绪,所以会失去代表客户端的client-socket,该client-socket会被退出到select的文件描述符列表中,而后服务端主线程又调用select进入监督状态,此时是同时监督listen-socketclient-socket,后续主线程从select返回后,判断如果是listen-socket准备就绪,则将失去的client-socket退出select的文件描述符列表,如果是client-socket准备就绪,则解决对应的客户端的申请。单Reactor单线程模型能够用下图进行示意。

Reactor单线程模型中,只有一个Reactor,负责调用select来监督listen-socketclient-socket,当有socket准备就绪时,称有事件产生,如果是listen-socket准备就绪,则产生了连贯事件,如果是client-socket准备就绪,则产生了读写事件,不同的事件由dispatch来散发到不同的模块进行解决,连贯事件由Acceptor来获取client-socket并退出到select的文件描述符列表,读写事件由Handler来解决即执行客户端的申请并响应。

Reactor单线程模型的单线程体现在上述的操作均都是产生在主线程中,即当同时有连贯事件和读写事件准备就绪时,单Reactor单线程模型会串行的解决连贯事件和读写事件,该模型的长处就是简略且没有并发问题,毛病就是通常解决连贯事件很快然而解决读写事件会较慢从而造成CPU资源被节约,假若解决读写事件也很快,那么单Reactor单线程模型会是一个优良的抉择,恰好在Redis中,因为数据都是存储在内存中,Redis服务端响应客户端的读写事件的速度是很快的,所以,Redis中的单线程模型,理论就是单Reactor单线程模型

二. Redis中的文件事件处理器

Redis服务端是通过listen-socket来获取客户端连贯,通过client-socket来解决客户端申请,listen-socketclient-socket可连贯,可读或者可写时都会产生事件,称为文件事件,即文件事件是Redis服务端对socket的操作的形象。Redis有一个文件事件处理器来解决文件事件,示意图如下所示。

Redis服务端会在I/O多路复用器中将socket准备就绪的操作入队列,所以准备就绪的操作会作为文件事件有序的被文件事件分派器分派到事件处理器的不同模块解决。 Redis中的文件事件处理器就是一个单Reactor单线程模型,并且单线程是体现在事件处理器解决不同的事件时是单线程的。

上面给出客户端申请Redis服务端的流程示意图。

最初对上图做如下几点阐明。

  • 如果客户端的connect-socket执行connect操作,或者客户端向Redis发动写申请,那么对应的socket会产生AE_READABLE事件;
  • 如果客户端向Redis发动读申请,那么对应的socket会产生 AE_WRITABLE事件;
  • 上述流程图中,编号雷同示意同一个申请的解决步骤。

总结

Redis的单线程模型,就是单Reactor单线程模型,Redis应用I/O多路复用,在单线程中轮询socket,并将对Redis库的建设连贯,敞开连贯,读数据和写数据申请都转换成了文件事件,最初Redis还应用其实现的文件事件分派器和事件处理器来解决不同的事件,整体执行效率高,还节俭了多线程的开销。