你好,我是蒋德钧。
明天,咱们来探讨一个很多人都很关怀的问题:“为什么单线程的Redis能那么快?”
首先,我要和你厘清一个事实,咱们通常说,Redis是单线程,次要是指Redis的网络IO和键值对读写是由一个线程来实现的,这也是Redis对外提供键值存储服务的次要流程。但Redis的其余性能,比方长久化、异步删除、集群数据同步等,其实是由额定的线程执行的。
所以,严格来说,Redis并不是单线程,然而咱们个别把Redis称为单线程高性能,这样显得“酷”些。接下来,我也会把Redis称为单线程模式。而且,这也会促使你紧接着发问:“为什么用单线程?为什么单线程能这么快?”
要弄明确这个问题,咱们就要深刻地学习下Redis的单线程设计机制以及多路复用机制。之后你在调优Redis性能时,也能更有针对性地防止会导致Redis单线程阻塞的操作,例如执行复杂度高的命令。
好了,话不多说,接下来,咱们就先来学习下Redis采纳单线程的起因。
Redis为什么用单线程?
要更好地了解Redis为什么用单线程,咱们就要先理解多线程的开销。
多线程的开销
日常写程序时,咱们常常会听到一种说法:“应用多线程,能够减少零碎吞吐率,或是能够减少零碎扩展性。”确实,对于一个多线程的零碎来说,在有正当的资源分配的状况下,能够减少零碎中解决申请操作的资源实体,进而晋升零碎可能同时解决的申请数,即吞吐率。上面的左图是咱们采纳多线程时所期待的后果。
然而,请你留神,通常状况下,在咱们采纳多线程后,如果没有良好的零碎设计,理论失去的后果,其实是右图所展现的那样。咱们刚开始减少线程数时,零碎吞吐率会减少,然而,再进一步减少线程时,零碎吞吐率就增长缓慢了,有时甚至还会呈现降落的状况。
为什么会呈现这种状况呢?一个要害的瓶颈在于,零碎中通常会存在被多线程同时拜访的共享资源,比方一个共享的数据结构。当有多个线程要批改这个共享资源时,为了保障共享资源的正确性,就须要有额定的机制进行保障,而这个额定的机制,就会带来额定的开销。
拿Redis来说,在上节课中,我提到过,Redis有List的数据类型,并提供出队(LPOP)和入队(LPUSH)操作。假如Redis采纳多线程设计,如下图所示,当初有两个线程A和B,线程A对一个List做LPUSH操作,并对队列长度加1。同时,线程B对该List执行LPOP操作,并对队列长度减1。为了保障队列长度的正确性,Redis须要让线程A和B的LPUSH和LPOP串行执行,这样一来,Redis能够无误地记录它们对List长度的批改。否则,咱们可能就会失去谬误的长度后果。这就是多线程编程模式面临的共享资源的并发访问控制问题。
并发访问控制始终是多线程开发中的一个难点问题,如果没有精密的设计,比如说,只是简略地采纳一个粗粒度互斥锁,就会呈现不现实的后果:即便减少了线程,大部分线程也在期待获取访问共享资源的互斥锁,并行变串行,零碎吞吐率并没有随着线程的减少而减少。
而且,采纳多线程开发个别会引入同步原语来爱护共享资源的并发拜访,这也会升高零碎代码的易调试性和可维护性。为了防止这些问题,Redis间接采纳了单线程模式。
讲到这里,你应该曾经明确了“Redis为什么用单线程”,那么,接下来,咱们就来看看,为什么单线程Redis能取得高性能。
单线程Redis为什么那么快?
通常来说,单线程的解决能力要比多线程差很多,然而Redis却能应用单线程模型达到每秒数十万级别的解决能力,这是为什么呢?其实,这是Redis多方面设计抉择的一个综合后果。
一方面,Redis的大部分操作在内存上实现,再加上它采纳了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要起因。另一方面,就是Redis采纳了多路复用机制,使其在网络IO操作中能并发解决大量的客户端申请,实现高吞吐率。接下来,咱们就重点学习下多路复用机制。
首先,咱们要弄明确网络操作的根本IO模型和潜在的阻塞点。毕竟,Redis采纳单线程进行IO,如果线程被阻塞了,就无奈进行多路复用了。
根本IO模型与阻塞点
你还记得我在第一节课介绍的具备网络框架的SimpleKV吗?
以Get申请为例,SimpleKV为了解决一个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线程的三个问题:“Redis真的只有单线程吗?”“为什么用单线程?”“单线程为什么这么快?”
当初,咱们晓得了,Redis单线程是指它对网络IO和数据读写的操作采纳了一个线程,而采纳单线程的一个外围起因是防止多线程开发的并发管制问题。单线程的Redis也能取得高性能,跟多路复用的IO模型密切相关,因为这防止了accept()和send()/recv()潜在的网络IO操作阻塞点。
搞懂了这些,你就走在了很多人的后面。如果你身边还有不分明这几个问题的敌人,欢送你分享给他/她,解决他们的困惑。
另外,我也剧透下,可能你也留神到了,2020年5月,Redis 6.0的稳定版公布了,Redis 6.0中提出了多线程模型。那么,这个多线程模型和这节课所说的IO模型有什么关联?会引入简单的并发管制问题吗?会给Redis 6.0带来多大晋升?对于这些问题,我会在前面的课程中和你具体介绍。
每课一问
我给你提个小问题,在“Redis根本IO模型”图中,你感觉还有哪些潜在的性能瓶颈吗?欢送在留言区写下你的思考和答案,咱们一起交换探讨。