Redis真的是单线程吗?

453 阅读9分钟

Redis真的是单线程吗?

这种问法其实并不严谨,为啥这么说呢?

Redis的版本很多3.x、4.x、6.x,版本不同架构也是不同的,不限定版本问是否单线程也不太严谨。

版本3.x ,最早版本,也就是大家口口相传的redis是单线程,阳哥2016年讲解的redis就是3.X的版本。

版本4.x,严格意义来说也不是单线程,而是负责处理客户端请求的线程是单线程,但是开始加了点多线程的东西(异步删除)。

2020年5月版本的6.0.x后及2022年出的7.0版本后,告别了大家印象中的单线程,用一种全新的多线程来解决问题。

所以说,当别人问我们redis是单线程的时候,我们应该针对具体的版本进行回答

下面是一张Redis关键版本的时间线图: 525.7.png

1. 我么常说的单线程是什么意思?

Redis是单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。

525.8.png

注意:

但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的

2. Redis3.x版本是单线程为啥还这么快

内存优化:

Redis 将数据存储在内存中,而内存访问速度远远高于磁盘访问速度。

Redis 使用紧凑的数据结构和数据压缩技术,减少了存储空间的占用。

Redis 采用了特定的数据结构,如哈希表、跳表和位图等,用于高效存储和处理不同类型的数据。

非阻塞I/O:

  1. Redis 使用了非阻塞式I/O模型,通过单个线程处理多个客户端连接和I/O操作。
  2. 在I/O操作等待数据传输的过程中,Redis可以继续处理其他请求,充分利用CPU资源。
  3. 非阻塞I/O避免了线程切换的开销,提高了并发性能。

单线程优化:

  1. 单线程避免了多线程之间的线程切换和锁竞争带来的开销。
  2. Redis 的单线程模型简化了数据访问的逻辑,避免了复杂的并发控制问题。
  3. 单线程模型使得Redis的内部操作是原子性的,无需考虑并发写冲突的问题。

异步操作:

  1. Redis 支持异步操作,如批量操作、管道(pipeline)操作和订阅/发布等。
  2. 异步操作允许客户端一次发送多个请求,减少了网络通信的开销。
  3. 异步操作可以批量执行多个命令,减少了客户端和服务器之间的往返次数。

高效的网络通信:

  1. Redis 使用高性能的网络库,如epoll、kqueue等,提高了网络通信的效率。
  2. Redis采用了自定义的协议,使用简单且高效,减少了数据传输的开销。
  3. Redis的客户端和服务器之间采用TCP连接,保证了可靠性和稳定性。

对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。

3. 既然单线程这么好,为什么Redis又逐渐加入了多线程?

3.1. 单线程也有单线程的苦恼呀!

尽管 Redis 在早期版本中采用了单线程模型,并且在许多场景下表现出色,但随着数据量和并发访问的增加,单线程模型也存在一些潜在的限制。因此,Redis 在较新的版本中引入了多线程的特性,主要有以下几个原因:

  1. 多核利用:单线程模型无法充分利用多核处理器的计算能力。在单线程模型下,无法并行处理多个任务,导致某些操作的性能受限。通过引入多线程模型,可以将不同任务分配给不同的线程,在多核处理器上并行执行,提高整体的并发处理能力。
  2. 长时阻塞问题:在某些情况下,Redis 的单线程模型可能面临长时阻塞的问题。例如,当执行慢查询、持久化操作(如RDB快照或AOF重写)或大规模数据迁移时,这些操作可能会导致Redis无法及时响应其他请求。通过引入多线程模型,可以将这些长时阻塞的操作分配给独立的线程,保持主线程的高响应性。
  3. I/O密集型操作:尽管 Redis 的主要瓶颈通常是CPU而不是I/O,但在某些场景下,I/O密集型操作(如大量的网络请求)可能成为性能瓶颈。通过引入多线程模型,可以在主线程处理CPU密集型操作的同时,将I/O密集型操作分配给其他线程,以提高整体的处理效率。

3.2. Redis单线程模式

3.3. 为什么引入?

查看官方文档发现:

  • 在使用 Redis 时,Redis 主要受限是在内存和网络上,CPU 几乎没有性能瓶颈的问题。
  • 以Linux 系统为例子,在Linux系统上Redis 通过 pipelining 可以处理 100w 个请求每秒,而应用程序的计算复杂度主要是 O(N) 或 O(log(N)) ,不会消耗太多 CPU。
  • 使用了单线程后,提高了可维护性。多线程模型在某些方面表现优异,却增加了程序执行顺序的不确定性,并且带来了并发读写的一系列问题,增加了系统复杂度。同时因为线程切换、加解锁,甚至死锁,造成一定的性能损耗。
  • Redis 通过 AE 事件模型以及 IO 多路复用等技术,拥有超高的处理性能,因此没有使用多线程的必要。

可以看出,Redis对CPU计算力的要求并不迫切,相反单线程机制让 Redis 内部实现的复杂度大大降低,同时降低了因为上下文切换和资源竞争造成的性能损耗。那既然单线程这么好用,为什么要引入多线程模式。

4. Redis6.x多线程特性和IO多路复用

4.1. 原因

近年来底层网络硬件性能越来越好,Redis 的性能瓶颈逐渐体现在网络 I/O 的读写上,单个线程处理网络 I/O 读写的速度跟不上底层网络硬件执行的速度。

在Redis 6/7中,引入了多线程是一个备受关注的新特性。之前,Redis一直以其单线程架构而闻名。尽管某些命令操作可以使用后台线程或子进程执行(如数据删除、快照生成、AOF重写),但从网络IO处理到实际的读写命令处理,都是由单个线程完成的。

为了应对这个问题,Redis 6/7采用了多个IO线程来处理网络请求,提高了网络请求处理的并行度。

然而,需要注意的是,Redis的多IO线程仅用于处理网络请求,对于读写操作命令,Redis仍然使用单线程来处理。这是因为在Redis处理请求时,网络处理经常成为瓶颈。通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。然而,为了保证Lua脚本和事务的原子性,继续使用单线程执行命令操作是必要的。这样一来,Redis的线程模型实现就更为简单,无需额外开发多线程互斥加锁机制(无论如何实现加锁操作)。

因此,Redis 6/7中的多线程特性主要针对网络IO处理,而仍然保持了单线程处理读写操作的优势。这种组合使得Redis在性能和简单性之间找到了一个平衡点。

4.2. 优化方法

  • 提高网络IO性能:这种方法旨在改善Redis执行期间读写网络的性能瓶颈。一种常见的实现方式是使用DPDK(Data Plane Development Kit)来替代内核网络栈。DPDK是一个开源软件开发套件,它提供了用户空间网络堆栈,可以实现更高的网络数据包处理性能。通过使用DPDK,可以绕过内核的网络协议栈,直接在用户空间进行网络数据包处理,从而提高Redis的网络IO性能。
  • 使用多线程:通过引入多线程,Redis可以更充分地利用多核CPU的资源。相比之下,主线程只能利用一个CPU核心。通过使用多线程,可以将Redis的负载分散到多个线程中,以实现并行处理。这样一来,多个线程可以同时执行任务,从而提高Redis的整体性能。类似的实现案例是Memcached,它也使用了多线程来提高性能。

协议栈优化是一种改善网络性能的方法,与Redis的关系并不直接。因此,最方便和高效的方式是支持多线程。通过支持多线程,Redis可以充分利用多核CPU资源,并将同步IO的读写负载分摊到多个线程中,从而降低延迟和提高性能。

525.10.png

4.3. IO多路复用执行流程

525.9.png

具体步骤如下:

  • 主线程建立连接,并接受数据,并将获取的 socket 数据放入等待队列;
  • 通过轮询的方式将 socket读取出来并分配给 IO 线程;
  • 之后主线程保持阻塞,一直等到 IO 线程完成 socket 读取和解析;
  • I/O 线程读取和解析完成之后,返回给主线程 ,主线程开始执行 Redis 命令;
  • 执行完Redis命令后,主线程阻塞,直到IO 线程完成 结果回写到socket 的工作;
  • 主线程清空已完成的队列,等待客户端新的请求。

本质上是将主线程 IO 读写的这个操作 独立出来,单独交给一个I/O线程组处理。 这样多个 socket 读写可以并行执行,整体效率也就提高了。同时注意 Redis 命令还是主线程串行执行。

注:本文参考了以下内容www.cnblogs.com/wzh2010/p/1…