单线程为啥还这么快
redis 是单线程吗?
Redis的工作原理
Redis是一个高效的内存数据库,它支持多种数据结构,如字符串、哈希、列表、集合等。它的核心思想是将数据存储在内存中,通过快速的读写操作来提升性能。为了做到这一点,Redis设计得非常轻量,性能非常高。
单线程 vs 多线程
在解释Redis是否是单线程之前,我们需要理解“单线程”和“多线程”这两个概念。
- 单线程:在单线程的应用程序中,所有的任务都是由一个线程按顺序执行的。也就是说,一个时间点只能执行一个任务,不会同时处理多个任务。
- 多线程:在多线程的应用程序中,多个任务可以同时执行(由不同的线程处理),从而提高效率。
Redis是单线程吗?
是的,Redis是单线程的。
想象一下,你在一个餐厅点餐。这个餐厅有多个客户,但服务员只有一个。服务员并不会一个接一个地为每个客户服务(即排队等候),而是监听多个桌子,看到有客户招手时,就过去服务。
在Redis中,服务员就是主线程,而桌子就是客户端连接,服务员会“监听”多个桌子的请求,并在有请求时立即去处理它。
单线程处理请求的具体过程
现在,让我们来详细看一下,Redis如何用单线程来处理一个客户端的请求,可以分为以下五个阶段:
- 初始化阶段:Redis启动时,会初始化单线程的事件循环(event loop)。这个循环的任务是监听所有的客户端连接,并且分配相应的资源。
- 监听客户端请求:当客户端连接到Redis时,Redis通过
select
、epoll
等系统调用来监听这个连接的输入输出。主线程将等待直到有活动的客户端请求。 - 处理请求:当有请求到达时,Redis会马上“醒来”,然后主线程处理该请求。这时,Redis会解析命令,执行相应的操作,比如写入数据库、更新数据等。
- 响应客户端:处理完请求后,Redis会将结果返回给客户端。例如,如果是一个
GET
命令,Redis会返回存储在内存中的数据;如果是SET
命令,Redis会确认操作是否成功并返回。 - 继续监听其他请求:在处理完当前请求后,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 不同版本之间的变化。