共计 4380 个字符,预计需要花费 11 分钟才能阅读完成。
原文地址:https://time.geekbang.org/col…
集体博客地址:http://njpkhuan.cn/archives/r…
你好,我是蒋德钧。
明天,咱们来探讨一个很多人都很关怀的问题:“为什么单线程的 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 模型与阻塞点
以 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 带来多大晋升?对于这些问题,我会在前面的课程中和你具体介绍。