单线程Redis 为什么那么快
通常来说,单线程的处理能力要比多线程差很多,但是Redis却能使用单线程模型,达到每秒数十万级别的处理能力,这是为什么呢?
其实,这是Redis多方面设计选择的一个综合结果。一方面,Redis的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。另一方面,就是Redis采用了多路复用机制,使其在网络IO操作中,能并发处理大量的客户端请求,实现高吞吐率
接下来,重点说下多路复用机制。
首先,我们要弄明白网络操作的基本IO模型和潜在的阻塞点。毕竟,Redis采用单线程进行IO,如果线程被阻塞了,就无法进行多路复用了。
以Get请求为例,为了处理一个Get请求,需要监听客户端请求(bind/listen),和客户端建立连接(accept),从socket中读取请求(recv),解析客户端发送请求(parse),根据请求类型读取键值数据(get),最后给客户端返回结果,即向socket中写回数据(send)。
其中,bind/listen、accept、recv、parse和send属于网络IO处理,而get属于键值数据操作。既然Redis是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。但是,在这里的网络IO操作中,有潜在的阻塞点,分别是 建立连接(accept()) 和 读取请求(recv()) 。
当Redis监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在accept()函数这里,导致其他客户端无法和Redis建立连接。类似的,当Redis通过recv()
从一个客户端读取数据时,如果数据一直没有到达,Redis也会一直阻塞在recv()
。
这就导致Redis整个线程阻塞,无法处理其他客户端请求,效率很低。不过,幸运的是,socket网络模型本身支持非阻塞模式。
非阻塞模式
Socket网络模型的非阻塞模式设计,主要体现在三个关键的函数调用上,如果想要使用socket非阻塞模式,就必须要了解这三个函数的调用返回类型和设置模式。
在socket模型中,不同操作调用后,会返回不同的套接字类型。
socket()方法会返回主动套接字,然后调用listen()
方法,将主动套接字转化为监听套接字。此时,可以监听来自客户端的连接请求。最后,调用accept()
方法接收到达的客户端连接,并返回已连接套接字。
针对监听套接字,我们可以设置非阻塞模式。当Redis调用accept()
,但一直未有连接请求到达时,Redis线程可以返回处理其他操作,而不用一直等待。但是,你要注意的是,调用accept()时,已经存在监听套接字了。
虽然Redis线程可以不用继续等待,但是总得有机制继续在监听套接字上,等待后续连接请求,并在有请求时通知Redis。类似的,我们也可以针对,已连接套接字设置非阻塞模式。Redis调用recv()
后,如果已连接套接字上,一直没有数据到达,Redis线程同样可以返回处理其他操作。
我们也需要有机制,继续监听已连接套接字,并在有数据达到时通知Redis。这样才能保证Redis线程,既不会像基本IO模型中一直在阻塞点等待,也不会导致Redis无法处理实际到达的连接请求或数据。
到此,Linux中的IO多路复用机制就要出现了
基于多路复用的高性能 I/O 模型
Linux中的IO多路复用机制,是指一个线程处理多个IO流,就是我们经常听到的select/epoll
机制。
简单来说,在Redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给Redis线程处理,这就实现了一个Redis线程处理多个IO流的效果。如下图所示,就是基于多路复用的Redis IO模型
多个FD就是刚才所说的监听套接字。Redis网络框架调用epoll机制,让内核监听这些套接字。此时,Redis线程不会阻塞在某一个特定的监听,或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis可以同时和多个客户端连接并处理请求,从而提升并发性。为了在请求到达时能通知到Redis线程,select/epoll
提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数
那么,回调机制是怎么工作的呢?
其实,select/epoll
一旦监测到FD上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis单线程对该事件队列,不断进行处理。这样一来,Redis无需一直轮询,是否有请求实际发生,这就可以避免造成CPU资源浪费。同时,Redis在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为Redis一直在对事件队列进行处理,所以能及时响应客户端请求,提升Redis的响应性能。
我再以连接请求和读数据请求为例,具体解释一下。
这两个请求分别对应Accept事件和Read事件,Redis分别对这两个事件注册accept和get回调函数。当Linux内核监听到有连接请求或读数据请求时,就会触发Accept事件和Read事件,此时,内核就会回调Redis相应的accept和get函数进行处理。
这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于Linux内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于Redis单线程),效率也能提升。
不过,需要注意的是,即使你的应用场景中,部署了不同的操作系统,多路复用机制也是适用的。因为这个机制的实现有很多种,既有基于Linux系统下的select和epoll实现,也有基于FreeBSD 的 kqueue 实现,以及基于Solaris的evport实现
这样,你可以根据Redis实际运行的操作系统,选择相应的多路复用实现。
通过刚才的学习,现在应该了解了Redis单线程了吧,是指它对网络IO和数据读写的操作,采用了一个线程,而采用单线程的一个核心原因,是避免多线程开发的并发控制问题。单线程的Redis也能获得高性能,跟多路复用的IO模型密切相关,因为这避免了accept()
和send()/recv()
潜在的网络IO操作阻塞点。
了解更多可扫码关注公众号