共计 5081 个字符,预计需要花费 13 分钟才能阅读完成。
前言
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-socket,listen-socket 会绑定服务端的 ip 和port,而后服务端进入监听状态;
- 客户端申请服务端时,客户端创立 connect-socket,connect-socket 形容了其要连贯的服务端的 listen-socket,而后connect-socket 向listen-socket发动连贯申请;
- connect-socket与 listen-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/O 和I/O多路复用
同步阻塞 I/O 是用户过程调用 read 时发动的 I/O 操作,此时用户过程由用户态转换到内核态,只有在内核态中将 I/O 操作执行完后,才会从内核态切换回用户态,这期间用户过程会始终阻塞。同步阻塞I/O(Blocking IO,BIO)示意图如下。
同步非阻塞 I/O 是用户过程调用 read 时,用户过程由用户态转换到内核态后,此时如果没有系统资源数据可能被读取到内核缓冲区中,返回 read 失败,并从内核态切换回用户态。也就是用户过程发动 I/O 操作后会立刻失去一个操作后果,同时用户过程须要在 read 失败时始终反复的发动 read,直至read 胜利。同步非阻塞I/O(Non-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 操作返回并失去代表 Client1 的client1-socket,随后 Server 在主线程中解决 Client1 的申请,此时 Client2 创立 connect2-socket 并发动 connect 操作,因为 Server 主线程正在解决 Client1 的申请,所以 Server 此时不会立刻与 Client2 建设连贯,等到 Server 主线程中解决完了 Client1 的申请并断开与 Client1 的连贯后,此时 Server 才会再与 Client2 建设连贯。上述的客户端申请服务端的模型,实质就是同步阻塞 I/O 模型,对于服务端来说,这种模型有两个问题,如下所示。
- 服务端是单线程的,同一时间只能在服务端主线程中监听到一个客户端建设连贯的申请,并且只会在解决完以后建设了连贯的客户端的申请后,才会持续与下一个客户端建设连贯;
- 服务端的 listen-socket 的accept操作是阻塞的,服务端与客户端建设连贯后,client-socket的 read 和write操作是阻塞的,换句话说,服务端要么阻塞在 listen-socket 的accept操作上,要么阻塞在 client-socket 的read和 write 操作上。
那么单 Reactor 单线程模型在引入了多路复用 I/O 后,对下面第二个问题进行了优化:服务端主线程中应用 select 或者 epoll 等操作,来同时监督 listen-socket 和client-socket。以 select 举例,服务端主线程中一开始会在 select 的文件描述符列表中增加 listen-socket,随后调用select 进入监督状态(此时主线程阻塞在 select 上),此时如果客户端的 connect-socket 发动了 connect 操作,服务端主线程就会从 select 上返回,并且判断是 listen-socket 准备就绪,所以会失去代表客户端的 client-socket,该client-socket 会被退出到 select 的文件描述符列表中,而后服务端主线程又调用 select 进入监督状态,此时是同时监督 listen-socket 和client-socket,后续主线程从 select 返回后,判断如果是 listen-socket 准备就绪,则将失去的 client-socket 退出 select 的文件描述符列表,如果是 client-socket 准备就绪,则解决对应的客户端的申请。单 Reactor 单线程模型能够用下图进行示意。
单 Reactor 单线程模型中,只有一个 Reactor,负责调用select 来监督 listen-socket 和client-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-socket和 client-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 还应用其实现的文件事件分派器和事件处理器来解决不同的事件,整体执行效率高,还节俭了多线程的开销。