1. 首页 > 百货

Redis为什么这么快 经典面试题

Redis有多快

根据官方基准测试,在具有平均硬件的Linux机器上运行的单个Redis实例通常可以为简单命令(O(N)或O(log(N)))实现8w+的QPS,使用流水线批处理可以达到100w。

从性能角度来看,Redis可以称为高性能的缓存解决方案。

Redis为什么这么快

面试时经常被问到Redis高性能的原因,典型回答是下面这些:

为什么Redis选择单线程?

上面回答了是单线程的,接着会问为啥采用单线程模型。

Redis的CPU通常不会成为性能瓶颈,因为通常情况下Redis要么受到内存限制,要么受到网络限制。例如,使用流水线技术,在平均Linux系统上运行的Redis甚至可以每秒处理100万个请求,因此,如果应用程序主要使用O(N)或O(log(N))命令,它几乎不会使用太多CPU。

这基本上意味着CPU通常不是数据库的瓶颈,因为大多数请求不会占用太多CPU资源,而是占用I/O资源。特别是对于Redis来说,如果不考虑像RDB/AOF这样的持久性方案,Redis是完全的内存操作,非常快速。Redis的真正性能瓶颈是网络I/O,即客户端和服务器之间的网络传输延迟,因此Redis选择了单线程的I/O多路复用来实现其核心网络模型。

「实际上选择单线程的更具体原因可以总结如下:」

Redis真的是单线程的吗?

在回答这个问题之前,我们需要澄清“单线程”概念的范围:它是否涵盖了核心网络模型或整个Redis?如果是前者,答案是肯定的。Redis的网络模型在v6.0之前一直是单线程的;如果是后者,答案是不。Redis早在v4.0版本中就引入了多线程。

单线程网络模型

从Redis v1.0到v6.0,Redis的核心网络模型一直是典型的单Reactor模型:使用epoll/select/kqueue等多路复用技术来处理事件(客户端请求)在单线程事件循环中,最后将响应数据写回客户端。

在这里有几个核心概念需要了解。

Redis内部实现了一个高性能事件库AE,基于epoll/select/kqueue/evport,用于为Linux/MacOS/FreeBSD/Solaris实现高性能事件循环模型。Redis的核心网络模型正式构建在AE之上,包括I/O多路复用和各种处理器绑定的注册,所有这些都是基于它实现的。

到这里,我们可以描述一个客户端从Redis请求命令的工作方式。

对于那些希望利用多核性能的人来说,官方的Redis解决方案简单而直接:在同一台机器上运行更多的Redis实例。事实上,为了保证高可用性,一个在线业务不太可能以独立运行的方式存在。更常见的是使用Redis分布式集群,具有多个节点和数据分片,以提高性能和确保高可用性。

多线程异步任务

如前所述,Redis在v4.0版本中引入了多线程来执行一些异步操作,主要用于非常耗时的命令。通过将这些命令的执行设置为异步,可以避免阻塞单线程事件循环。

我们知道Redis的DEL命令用于删除一个或多个键的存储值,它是一个阻塞命令。在大多数情况下,要删除的键不会存储太多值,最多几十个或几百个对象,因此可以快速执行。但如果要删除具有数百万个对象的非常大的键值对,则此命令可能会阻塞至少几秒钟,由于事件循环是单线程的,它会阻塞随后的其他事件,从而降低吞吐量。

Redis的作者antirez对解决这个问题进行了深思熟虑。起初,他提出了一个渐进式的解决方案:使用定时器和数据游标,他将逐步删除少量数据,例如1000个对象,最终清除所有数据。但这个解决方案存在一个致命的缺陷:如果其他客户端继续写入正在逐步删除的键,而且删除速度跟不上写入的数据,那么内存将无休止地被消耗,这个问题通过一个巧妙的解决方案得以解决,但这个实现使Redis更加复杂。多线程似乎是一个牢不可破的解决方案:简单且容易理解。因此,最终,antirez选择引入多线程来执行这类非阻塞命令。antirez在他的博客中更多地思考了这个问题:懒惰的Redis是更好的Redis。

因此,在Redis v4.0之后,已添加了一些非阻塞命令,如UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC等,它们会在后台线程中执行,不会阻塞主线程事件循环。这使得Redis可以更好地应对一些特定情况下的命令处理。

多线程异步任务的主要特点:

总结 Redis的网络模型是单线程的,这意味着它使用单个事件循环来处理所有客户端请求。这个设计的优点是简单性和可维护性,但需要谨慎处理一些可能导致事件循环阻塞的命令。

为了处理一些非常耗时的命令,Redis v4.0引入了多线程异步任务。这些异步任务在后台线程中执行,不会阻塞主线程的事件循环,从而提高了Redis的吞吐量和可用性。

总而言之,Redis的单线程事件循环和多线程异步任务的设计是为了在性能和简单性之间取得平衡,以满足各种不同用例的需求。理解Redis的这些基本原理对于使用Redis进行高性能数据存储和缓存非常重要。

多线程网络模型

正如前面提到的,Redis最初选择了单线程的网络模型,原因是CPU通常不是性能瓶颈,瓶颈往往是内存和网络,因此单线程足够了。那么为什么Redis现在引入了多线程呢?简单的事实是Redis的网络I/O瓶颈变得越来越明显。

随着互联网的快速增长,互联网业务系统处理越来越多的在线流量,而Redis的单线程模式导致系统在网络I/O上消耗了大量CPU时间,从而降低了吞吐量。提高Redis性能有两种方式:

后者依赖于硬件的发展,目前尚无法解决。因此,我们只能从前者入手,网络I/O的优化可以分为两个方向:

零拷贝技术存在局限性,无法完全适应像Redis这样的复杂网络I/O场景。DPDK技术通过绕过内核栈来绕过NIC I/O,过于复杂,需要内核甚至硬件的支持。

因此,充分利用多个核心是优化网络I/O最具成本效益的方式。

在6.0版本之后,Redis正式将多线程引入核心网络模型中,也称为I/O线程,现在Redis具有真正的多线程模型。在前面的部分中,我们了解了Redis 6.0之前的单线程事件循环模型,实际上是一个非常经典的反应器模型。

反应器模式在Linux平台上的大多数主流高性能网络库/框架中都有应用,比如netty、libevent、libuv、POE(Perl)、Twisted(Python)等。

反应器模式实际上是指使用I/O多路复用(I/O multiplexing)+非阻塞I/O(non-blocking I/O)模式。

Redis的核心网络模型,直到6.0版本,都是单一的反应器模型:所有事件都在单一线程中处理,尽管在4.0版本中引入了多线程,但更多是用于特定场景的补丁(删除超大键值等),不能被视为核心网络模型的多线程。

一般来说,单一反应器模型,在引入多线程后,会演变为多反应器模型,具有以下基本工作模型。

与单一线程事件循环不同,这种模式有多个线程(子反应器),每个线程维护一个独立的事件循环,主反应器接收新连接并将其分发给子反应器进行独立处理,而子反应器则将响应写回客户端。

多反应器模式通常可以等同于Master-Workers模式,比如Nginx和Memcached使用这种多线程模型,尽管项目之间的实现细节略有不同,但总体模式基本一致。

Redis多线程网络模型设计

Redis也实现了多线程,但不是标准的多反应器/主工作模式。让我们先看一下Redis多线程网络模型的一般设计。

大部分逻辑与之前的单线程模型相同,唯一的改变是将读取客户端请求和写回响应数据的逻辑异步化到I/O线程中。这里需要特别注意的是,I/O线程只负责读取和解析客户端命令,实际的命令执行最终是在主线程上完成的。

总结

当面试官再问Redis为啥这么快时别傻傻再回答Redis是单线程了,否则只能回去等通知了。

Redis的多线程网络模型通过将读取和写回数据的任务异步化,以及更好地利用多核CPU,从而提高了Redis在处理大量在线流量时的性能表现。

1.「多线程设计」:

2.「异步读写」:Redis的多线程模型异步化了读取客户端请求和写回响应数据的过程。客户端请求首先被放入待读取队列,然后由I/O线程读取。执行命令仍然在主线程上进行,但这种异步化提高了系统的并发性和吞吐量。

本网站的文章部分内容可能来源于网络和网友发布,仅供大家学习与参考,如有侵权,请联系站长进行删除处理,不代表本网站立场,转载者并注明出处:https://www.jmbhsh.com/baihuo725/34427.html

联系我们

QQ号:***

微信号:***

工作日:9:30-18:30,节假日休息