Redis作者都说这个文章讲懂了高性能IO模块

325 阅读7分钟

为什么单线程Redis能那么快?

Redis的真实面目

都说Redis是单线程高性能的,但是实际上Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程完成的,当然Redis的这也是Redis对外提供简直存储服务最主要的所在.例如持久化/异步删除/集群数据同步等都是由额外的线程执行的.


为什么用单线程?

多线程开销

使用多线程可以增加系统吞吐率或者是增加系统的扩展性,确实在资源分配合理的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够给同时处理的请求数.

但是如果没有良好的系统设计就会出现下面的情况:

  1. 刚开始增加线程时系统的吞吐率得到提升.
  2. 近一步增加线程,系统吞吐率增长缓慢,甚至下降.

开辟了更多的线程为什么反而变慢?

一个关键的因素是系统中通常会出现被多线程同时访问的共享资源,可以是一个文件/一个数组或者是一个共享的数据结构.
为了保证共享资源的正确性,就要有额外的机制进行保证,这也带来了额外的开销.
这就是多线程编程模式带来的共享资源的并发访问控制问题.

为了避免多线程带了的额外开销及并发访问控制问题,Redis直接采用单线程模式.


既然使用单线程,为什么还能那么快?

一方面,Redis的大部分操作都是在运行内存中完成的,再加上它采用了高效的数据结构,这是它实现高性能的一个重要原因.
另一方面,就是Redis采用了多路复用机制,使得它在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率.

在了解多路复用机制之前,应该确定的一点就是,如果线程被阻塞了,将无法进行多路复用.

基本IO模型与阻塞点

以redis中的get请求为例,基本IO模型如下图所示: image.png

既然Redis是单线程,那么,最基本的一种实现是在一个线程中依次执行上述操作.

但是,在网络IO处理操作中有潜在的阻塞点,分别是accept()和revc().
当redis监听到一个客户端有连接请求,但一直未能成功建立连接时会阻塞早accept()函数这儿,导致其他客户端无法和Redis建立连接.
类似的当Redis通过recv()从一个客户端读取数据时,如果数据一直没有到达,redis也会一直阻塞在recv()函数.

这就导致了Redis整个线程的阻塞,无法处理其他客户端的请求,性能变差.
不过幸运的是socket网络模型本身支持非阻塞模式.

非阻塞模式

非阻塞模式的设置主要体现在三个关键函数的调用上,如果想要使用socket非阻塞模式,就要了解这三个函数的调用返回类型和设置模式.

socket模型中,不同操作调用后悔返回不同的套接字类型.如下:

  • socket():返回主动套接字,然后调用listen()
  • listen():讲主动套接字转换为监听套接字,此时可以监听来自客户端的连接请求,然后调用accept()
  • accept():接受到达客户端的连接,并返回已连接套接字.

针对监听套接字,可以设置非阻塞模式:当redis调用accept()但一直没有连接请求到达时,redis线程可以返回处理其他操作,而不用一直等待.

虽然redis线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求,并在有请求时通知redis.

类似的,我们可以针对已连接套接字进行非阻塞模式的设置:redis调用revc()后,如果已连接套接字上一直没有数据到达,redis线程同样可以返回处理其他操作.

同样,我们也需要机制继续监听该已连接套接字,并在有数据到达时通知redis.

这样就保证了redis线程既不会像基本IO模型中一直在阻塞点等待,也不会导致redis无法处理实际到达的连接请求或数据.

基于多路复用的高性能IO模型(Reactor模式)

IO多路复用机制是指一个线程处理多个IO流,对于Linux系统来说就是select/epoll机制,该机制允许内核中同时存在多个监听套接字和已连接套接字,内核会一直监听这些套接字上的连接请求或数据请求.一旦有请求到达,就会交给redis线程处理,这就实现了一个redis线程处理多个IO流的效果.

如下图所示:

image.png

为了在请求到达时能够通知redis线程,多路复用机制提供了基于事件的回调机制,也就是针对不同的事件,调用相对应的函数进行操作.

回调机制(参考图进行理解)

过程:

  1. s/e一旦检测到套接字上有请求到达时就会触发相应的事件.
  2. 将这些事件放到一个事件队列
  3. redis单线程对该队列不断的进行处理

优点:

  1. redis无需一直轮询套接字去查询是否有请求实际发生,这样就避免了一个定时器对CPU资源的浪费.
  2. redis在对事件队列进行处理时针对不同的事件调用相应的函数进行处理,这样就实现了基于事件的回调.
  3. redis一直在对事件队列进行处理没有事件差,所以能及时相应客户端的请求,提升redis的响应性能.

连接请求过程释义:

  1. 内核监听到连接请求
  2. 触发Accept事件并入队
  3. 出队并调用accept()进行处理
  4. return

读取数据过程释义:

  1. 内核监听到读数据请求
  2. 触发Read事件并入队
  3. 出队并调用get()进行处理
  4. return
注意

不同的操作系统有不同的多路复用机制的实现,无需担忧不同的操作系统会对redis的性能产生影响,只不过我们大多数情况是在Linux系统中使用redis的,也就是基于s/e的多路复用机制.


可能面试会问到的问题

redis是单线程吗?

不是.
redis只是网络IO和数据读写是单线程的.

为什么用单线程?

避免多线程带来的额外开销以及并发访问控制问题,并且采用多路复用机制的单线程也能获得高性能,还能避免accept()/send()/recv()等方法潜在的网络IO阻塞问题.

单线程为什么那么快?

首先,redis大部分操作都是在内存中完成的,再加上它采用了高效的数据结构.
其次,采用了多路复用机制的IO模型,和基本IO模型相比,它避免了操作函数中潜在的线程阻塞问题,并且其中的事件回调机制避免了去轮询套接字带来的CPU开销,并且能一直对事件队列进行处理避免了潜在的时间差问题.