单线程为啥还这么快

redis 是单线程吗?

Redis的工作原理

Redis是一个高效的内存数据库,它支持多种数据结构,如字符串、哈希、列表、集合等。它的核心思想是将数据存储在内存中,通过快速的读写操作来提升性能。为了做到这一点,Redis设计得非常轻量,性能非常高。

单线程 vs 多线程

在解释Redis是否是单线程之前,我们需要理解“单线程”和“多线程”这两个概念。

  • 单线程:在单线程的应用程序中,所有的任务都是由一个线程按顺序执行的。也就是说,一个时间点只能执行一个任务,不会同时处理多个任务。
  • 多线程:在多线程的应用程序中,多个任务可以同时执行(由不同的线程处理),从而提高效率。

Redis是单线程吗?

是的,Redis是单线程的。

想象一下,你在一个餐厅点餐。这个餐厅有多个客户,但服务员只有一个。服务员并不会一个接一个地为每个客户服务(即排队等候),而是监听多个桌子,看到有客户招手时,就过去服务。

在Redis中,服务员就是主线程,而桌子就是客户端连接,服务员会“监听”多个桌子的请求,并在有请求时立即去处理它。

单线程处理请求的具体过程

现在,让我们来详细看一下,Redis如何用单线程来处理一个客户端的请求,可以分为以下五个阶段:

  1. 初始化阶段:Redis启动时,会初始化单线程的事件循环(event loop)。这个循环的任务是监听所有的客户端连接,并且分配相应的资源。
  2. 监听客户端请求:当客户端连接到Redis时,Redis通过selectepoll等系统调用来监听这个连接的输入输出。主线程将等待直到有活动的客户端请求。
  3. 处理请求:当有请求到达时,Redis会马上“醒来”,然后主线程处理该请求。这时,Redis会解析命令,执行相应的操作,比如写入数据库、更新数据等。
  4. 响应客户端:处理完请求后,Redis会将结果返回给客户端。例如,如果是一个GET命令,Redis会返回存储在内存中的数据;如果是SET命令,Redis会确认操作是否成功并返回。
  5. 继续监听其他请求:在处理完当前请求后,Redis会继续回到事件循环中,开始监听下一个客户端的请求,整个过程继续进行。

我们所说的“单线程”的含义,是指 Redis 本身的主线程是单线程的。但是 Redis 程序并不是单线程的,Redis 在启动的时候,会启动后台线程。

Redis 用主线程处理大部分的请求,但为了提高整体性能并处理某些特定的任务,Redis引入了后台线程(也叫BIO线程)。BIO线程的引入目的是为了解决一些需要阻塞、耗时的操作,这些操作如果交给主线程去执行,会导致主线程的阻塞,从而影响其他客户端的请求。这些后台线程并不会直接处理客户端请求,但它们可以执行一些需要较长时间的操作,从而避免阻塞主线程,确保主线程能够继续处理请求。

Redis 2.6 版本引入了两个BIO线程,它们主要用来处理以下两个任务:

  • 关闭文件操作(关闭客户端连接):在Redis处理客户端请求时,可能会有一些客户端连接关闭的操作。关闭连接和释放资源是一个IO密集型的操作,如果让主线程去做这件事,就会阻塞主线程,影响其他请求的处理。BIO线程负责处理这些任务。
  • AOF(Append-Only File)刷盘操作:AOF用于持久化Redis的数据,Redis会将每一个写操作追加到AOF文件中。刷盘操作是一个相对耗时的任务,如果这个任务阻塞在主线程上,就会影响性能。因此,Redis使用BIO线程来异步执行AOF刷盘,确保主线程不被阻塞。

在Redis 2.6 版本,BIO 线程的作用基本上就集中在这两个方面。但是随着Redis版本的更新,Redis 的后台线程得到了扩展。特别是在 Redis 4.0 版本以后,Redis 新增了一个非常重要的后台线程——lazyfree 线程。这个线程主要是用于异步释放内存,以便减少主线程的工作负担。

lazyfree 线程的作用主要体现在以下几个方面:

  • 异步删除大对象:当执行DEL命令删除一个大的集合(比如大的列表、哈希或集合)时,Redis并不会立即释放所有内存,而是会将这个删除任务交给lazyfree线程。lazyfree线程会异步地释放内存,避免大对象删除过程中的主线程阻塞。
  • 异步释放过期键:如果启用了过期键的处理,Redis需要定期检查和清理过期的键。在Redis 4.0版本之前,过期键的清理任务是由主线程负责的,这会带来性能上的负担。通过引入lazyfree线程,Redis将部分过期键的清理任务交给后台线程来完成,从而减少主线程的压力。
  • 异步合并集合操作:比如说,删除集合中的某些元素时,Redis不必立即进行合并操作,而是交给lazyfree线程来异步处理。

其实这些后台线程看起来更像是消费者,生产者把任务放到队列中,然后消费者从队列中取出任务并消费。

redis 采用单线程为啥还这么快?

很多人可能会感到困惑:如果 Redis 是单线程的,那么它处理大量请求时是不是会很慢?为什么它仍然能在高并发场景下表现得如此高效呢?其实,Redis 的高效性能并不依赖于多线程,而是依赖于它的设计特点:大部分操作都在内存中完成单线程模型避免了多线程之间的竞争、以及I/O 多路复用机制。我们接下来就从这三个方面逐一分析。

什么是单线程模型?

在分析之前我们先来聊聊什么是单线程模型。

在计算机中,“线程”是程序执行的最小单位。如果我们说一个系统是“单线程”的,意味着这个系统在任何时刻只有一个线程在执行任务。我们可以想象成,一个人在做一件事情时,不会被打断,只是不断专注于当前的任务。

比如,当你在用 Redis 执行一个 SET 操作时,这个请求会被单独的一个线程处理。其他请求要排队等候,直到这个线程处理完当前的请求,才会去处理下一个请求。

Redis 的大部分操作都在内存中完成

首先,我们要了解 Redis 是一个内存数据库。这意味着 Redis 将数据存储在内存中,而不是像传统数据库那样使用磁盘存储数据。内存相比磁盘来说访问速度要快得多,几乎是秒级的响应时间。每次 Redis 执行操作时,它几乎所有的工作都是在内存中进行的,读取和写入数据的速度都非常快。

举个例子来说,假设你向 Redis 写入一个键值对 SET name "Alice",Redis 不需要去硬盘读取文件,只需在内存中创建一个键值对,几乎没有延迟。相比之下,如果数据库存储在磁盘中,每次读写都需要通过 I/O 操作,速度自然会变慢。

因此,Redis 的高速响应和高并发处理能力,很大一部分得益于它将数据存储在内存中,避免了磁盘 I/O 带来的延迟。

Redis 采用单线程模型可以避免了多线程之间的竞争

接下来,我们要理解为什么 Redis 选择了单线程模型而不是多线程。首先,多线程程序会面临“线程竞争”问题。假设有多个线程同时访问共享资源(比如共享的数据),系统就需要通过锁机制来保证数据一致性。这种锁机制会带来额外的性能开销,特别是当并发量很高时,锁竞争会导致线程等待和上下文切换,降低系统效率。

Redis 选择使用单线程来避免这个问题。由于 Redis 只有一个线程,它的操作是顺序执行的,不会出现多个线程同时竞争 CPU 或数据的情况。单线程意味着每个请求都是按顺序处理的,不会有锁竞争的问题,性能开销也大大降低。

Redis 采用了 I/O 多路复用机制

第三个重要原因是 Redis 使用了 I/O 多路复用机制。这意味着 Redis 可以同时处理多个客户端请求,而不需要为每个请求创建一个独立的线程。传统的多线程模型会为每个客户端请求分配一个线程,这样会增加上下文切换的开销和资源消耗。而 Redis 使用 I/O 多路复用,让单线程就能同时处理多个请求。

I/O 多路复用的核心思想是 Redis 通过事件驱动的方式,利用操作系统提供的高效机制,轮询检查多个连接的状态,当某个连接有请求到达时,Redis 就会处理这个请求。由于 Redis 是单线程的,事件处理的任务是顺序执行的,因此它避免了多线程模型下的线程调度和上下文切换开销。

举个例子,假设你有很多客户排队等着你处理订单。传统方法是每个客户都安排一个独立的工作人(线程)来处理,这样如果有很多客户,人员就会很多,协调起来很麻烦。Redis 的 I/O 多路复用就像是一个人站在门口,看到有客户来就处理他们的订单,处理完一个后立刻处理下一个,这样避免了大量工作人员之间的切换。

所以综合来看,Redis 之所以能够在单线程的情况下还能这么快,主要得益于以下三点:

  • 大部分操作都在内存中完成:内存访问速度远快于磁盘,极大提高了操作效率。
  • 单线程模型避免了多线程竞争:避免了多线程中的锁竞争和上下文切换,从而减少了性能损耗。
  • I/O 多路复用机制:通过事件驱动,Redis 能在单线程内高效地处理多个并发请求,避免了多线程模型下的资源开销。

redis 6.0 之前为什么采用单线程?

事实上,Redis 官方给出的回答是:“CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络 I/O 的限制,所以 Redis 核心网络模型使用单线程并没有什么问题。”

为什么 CPU 不是瓶颈?

“CPU 并不是制约 Redis 性能表现的瓶颈”。这是什么意思呢?

通常情况下,我们可能会觉得 CPU 是决定程序性能的主要因素。我们都知道,如果一个程序有多个线程同时执行,它可以更好地利用多核 CPU 来分担工作,理论上可以更快地完成任务。可是,Redis 采用的是单线程,也就是说,它只使用一个 CPU 核心来处理所有的请求,这样似乎会浪费掉其他 CPU 核心的能力。

但实际上,Redis 的大多数操作都非常简单,比如读取或写入一个键值对,这些操作对 CPU 的需求是非常低的。比如,你让一个人搬几箱苹果,他每次搬一箱就可以了,搬得很快。但是,如果你让他搬几十箱重物,才会发现人手不足,需要更多的帮手(线程)。

Redis 的操作非常轻量,因此,单线程的 CPU 处理能力足够满足大部分操作,而不需要分配给多个线程。

内存和网络 I/O 才是瓶颈

Redis 官方还指出,瓶颈往往出现在内存大小和网络 I/O 上。为什么是内存和网络 I/O 呢?

首先,Redis 作为一个内存数据库,内存的大小决定了它能够存储的数据量。尽管 Redis 非常快速,但如果内存不够大,数据量过大时,Redis 的性能就会受到影响。

而对于网络 I/O 来说,网络带宽和延迟是影响 Redis 性能的关键因素。即使 Redis 能够快速地处理请求,如果网络的传输速度比较慢,或者带宽有限,数据传输的瓶颈就会降低 Redis 的总体性能。

另外,除了上面的考虑,使用单线程还可以减少线程切换的开销、加锁造成的性能消耗等。

Redis 6.0 之后为什么引入了多线程?

在 Redis 6.0 之前,我们知道 Redis 使用的是单线程模型,这一点也是 Redis 高性能的一个重要原因。然而,Redis 6.0 版本发布之后,引入了多线程,这到底是为什么呢?单线程的优点不是很多吗?为什么要引入复杂的多线程呢?

我们下面一起来看看吧。

随着网络硬件性能的提升,Redis 的瓶颈逐渐出现在网络 I/O上

要理解 Redis 为什么要引入多线程,我们首先需要了解 Redis 之前的瓶颈是什么。

Redis 6.0 之前,采用了单线程的设计,虽然单线程可以避免线程切换和锁竞争,但它也有一定的局限性。具体来说,Redis 在处理网络 I/O 时,单线程可能会遇到性能瓶颈

举个例子,假设你有一个高速公路,车速非常快。但是,如果路面只有一条车道,车流量大时,交通就会变得非常拥堵,车辆无法快速通过,性能大打折扣。对于 Redis 来说,虽然它能够非常快速地处理数据,但网络带宽和网络 I/O 的速度限制,成为了它的“拥堵点”。

随着硬件技术的进步,网络带宽越来越大,网络 I/O 的处理速度也在不断提升。单线程的 Redis 开始无法完全利用这些高速网络带宽,成为性能的瓶颈。因此,Redis 6.0 版本开始引入了多线程,以解决这个问题。

如何设置 Redis 的线程数?

引入了多线程之后,Redis 的线程数该如何设置呢?很多同学可能会觉得,线程数越多,性能就越好。但实际上,线程数并不是越多越好,设置不当反而可能带来性能上的负担。

Redis 官方给出的建议是:

  • 对于 4 核 CPU 的机器,建议设置 2 或 3 个线程
  • 对于 8 核 CPU 的机器,建议设置 6 个线程

也就是说线程数要少于机器核数。

Redis 6.0 默认启用 6 个线程

Redis 6.0 之后,在启动时默认会创建 6 个额外的线程。它们是:

  • bio_close_file、bio_aof_ fsync、bio_lazy_free 三个后台线程,分别异步处理关闭文件任务、AOF刷盘

任务、释放内存任务;

  • io_thd_1、io _thd_2、io_thd_3 三个线程:用来分担 Redis 网络 I/O 的压力。

当然,还有一个 Redis 的主线程 Redis-server,主要用来负责执行命令。

总结

这篇文章我们主要分析的是 redis 是单线程为什么还这么快的问题。除此之外,我们还讨论了 redis 的单线程模型、单线程模型的优点以及 redis 不同版本之间的变化。

发表评论

后才能评论