37DATA

解读Redis单线程读写性能高的原因

一、Redis是单线程的还是多线程的?

在学习和使用redis的时候,大家常常说的是redis是单线程的。其实这种说法并不是很严谨,Redis的版本有非常多,3.x、4.x、6.x,版本不同架构也是不同的,不限定版本问是否单线程不太严谨。
1)版本3.x,最早版本,也就是大家口口相传的redis是单线程。
2)版本4.x,严格意义上来说也不是单线程,而是负责客户端处理请求的线程是单线程,但是开始加了点多线程的东西(异步删除)
3)最新版本的6.0.x后,告别了刻板印象中的单线程,而采用了一种全新的多线程来解决问题。

Image

二、Redis性能快的原因

1)基于内存操作,Redis的所有数据都存在内存中,因此所有的运算都是内存级别的,所以它的性能比较高。
2)数据结构简单:Redis的数据结构都是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1),因此性能比较强。
3)多路复用和非阻塞I/O,Redis使用I/O多路复用来监听多个socket链接客户端,这样就可以使用一个线程链接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作。
4)主线程为单线程,避免上下文切换,因为是单线程模型,因此避免了不必要的上下文切换和多线程竞争(比如锁),这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生。

从IO并发性能提升来考虑:

①多进程

对于并发情况,假如一个进程不行,那搞多个进程不就可以同时处理多个客户端连接了么?

多进程这种方式的确可以解决了服务器在同一时间能处理多个客户端连接请求的问题,但是仍存在一些缺点:

fork()等系统调用会使得进程上下文进行切换,效率较低

进程创建的数量随着连接请求的增加而增加。比如10w个请求,就要fork 10w个进程,开销太大

进程与进程之间的地址空间是私有、独立的,使得进程之间的数据共享变得困难

②多线程

线程是运行在进程上下文的逻辑流,一个进程可以包含多个线程,多个线程运行在同一进程上下文中,因此可共享这个进程地址空间的所有内容,解决了进程与进程之间通信难的问题。

同时,由于一个线程的上下文要比一个进程的上下文小得多,所以线程的上下文切换,要比进程的上下文切换效率高得多。

③基于单进程的IO多路复用(select/poll/epoll)

简单理解就是:一个服务端进程可以同时处理多个套接字描述符。

多路:多个客户端连接(连接就是套接字描述符)

复用:使用单进程就能够实现同时处理多个客户端的连接

以上是通过增加进程和线程的数量来并发处理多个套接字,免不了上下文切换的开销,而IO多路复用只需要一个进程就能够处理多个套接字,从而解决了上下文切换的问题。

三、IO多路复用

IO多路复用这块我们要好好聊聊,重点讲讲~!!!

IO即为网络I/O,多路即为多个TCP连接,复用即为共用一个线程或者进程,模型最大的优势是系统开销小,不必创建也不必维护过多的线程或进程。

IO多路复用是经典的Reactor设计模式,有时也称为异步阻塞IO(异步指socket为non-blocking,堵塞指select堵塞),为常见的四种IO模型之一,其他三种分别是:同步堵塞IO、同步非堵塞IO、异步(非堵塞)IO

Image

Redis通过使用I/O多路复用程序来监听过个socket,文件事件处理器既实现高性能网络通讯模型,又可以很好的与Redis以单线程运行的模块进行对接,保持了Redis内部单线程设计的简单性。

文件事件处理器的四个部分:套接字、I/O多路复用程序、文件事件分发器、事件处理器

Image

1)套接字socket

文件事件就是对套接字的抽象,每当一个套接字准备好执行连接、写入、读取、关闭等操作时,都会产生一个文件事件。
因为一个Redis服务器会有多个客户端连接,也就是说有多个套接字,所以多个文件事件可能会并发出现。


2)I/O多路复用程序

I/O多路复用程序,负责监听多个套接字,按照处理顺序,将套接字存放在一个队列中。
套接字队列以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。
当上一个套接字产生的事件被处理完毕之后, I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字。
Redis的I/O模型主要是基于epoll实现的,不过也提供了select和kquque的实现,默认是采用epoll,一个客户端连接后,将这个文件描述符(connfd)放到一个数组里。

3)select

select 是操作系统提供的系统调用函数,select 只能监听 1024 个文件描述符,通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理,

不过,当 select 函数返回后,用户依然需要遍历刚刚提交给操作系统的 list。只不过,操作系统会将准备就绪的文件描述符做上标识,用户层将不会再有无意义的系统调用开销。

Image

4)poll

它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。

5)epoll

epoll 是最终的大 boss,它解决了 select 和 poll 的一些问题。

所以 epoll 主要就是针对这三点进行了改进。

1. 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。

2. 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。

3. 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

Image

具体,操作系统提供了这三个函数。

第一步,创建一个 epoll 句柄

int epoll_create(int size);
第二步,向内核添加、修改或删除要监控的文件描述符。

int epoll_ctl( int epfd, int op, int fd, struct epoll_event *event);
第三步,类似发起了 select() 调用

int epoll_wait( int epfd, struct epoll_event *events, int max events, int timeout);

6)最后总结下汇总3者的特点和区别

Image

6)文件事件分发器

文件事件分派器接受I/O多路复用程序传递的套接字,根据套接字的事件类型,调用相应的事件处理器进行处理。

7)事件处理器

连接应答处理器:对连接服务器的各个客户端进行应答。
命令请求处理器:接收客户端传来的命令请求。
命令回复处理器:向客户端返回命令的执行结果
复制处理器:当主服务器和从服务器进行复制操作时, 主从服务器都需要关联此处理器。