Redis 是多线程还是单线程?这是一个经典的面试题目,个人认为也是对Redis架构设计的一个简单概要。 本文主要是对 Redis 架构进行一个简要的梳理分析和概要,而不在细节上进行深入的探讨
总览
首先,Redis 并不是简单的多线程或是单线程,他既是一个单线程程序,又是一个多线程程序,两者的关系并非非此即彼,而且在其中还穿插着异步线程。我们可以通过一张简单的模型图来大致概括一下 Redis 的线程结构:
接下来,我们就通过 Redis 版本来一步步解释分析,我们现在的 Redis 架构是如何搭建起来的
Redis 的单线程
Redis的"单线程"主要是指Redis的网络IO 和键值对读写 是由一个线程 来完成的,Redis在处理客户端的请求时包括获取(socket读)、解析、执行、内容返回(socket写)等都由一个顺序串行的主线程处理(第一篇介绍过的那些原子的命令),这也是Redis对外提供键值存储服务的主要流程。
Redis采用Reactor模式 的网络模型,对于一个客户端请求,主线程负责一个完整的处理过程,如下图:
补充:
Reactor 模式是一种并发模型,在这种模型中,**主线程(Reactor)**避免了由于等待一或多个并发事件(比如 I/O 操作)的完成而无法继续工作的阻塞。通过使用非阻塞 I/O 操作和事件通知,主线程在并发操作完成时得到通知。
一般来说,Reactor 模式的工作流程如下:
- 应用程序将需要**监听的 I/O 事件(比如 socket 的可读、可写事件)注册到 Reactor 中,并且关联对应的处理事件。
- Reactor 不断地轮询这些事件,当某个事件到达的时候(比如 socket 中有数据可读),Reactor 将这个事件对应的处理事件唤醒,交由一个工作线程(或者称之为 EventHandler)去处理。
- 工作线程处理完相应的事件后,通知 Reactor 继续监听这个事件。
这种模型非常适合于大并发,少逻辑的网络程序中,比如 Nginx 中就用到了这种设计模式。通过这种方式,一个线程可以处理很多连接的事件,而不需要为每个连接都创建一个线程,这样就可以避免线程切换的开销,并且可以更有效地使用系统资源。
Redis 的异步
Redis 作为一款内存型数据库,大多数读写操作的执行速度都非常快。然而,仍有一些操作相对耗时,例如持久化、空间清理、删除大对象(Big Key)等。这类操作如果在主线程中同步执行,可能会显著阻塞 Redis 对后续请求的响应,从而影响整体性能。
通过分析可以发现,这些操作往往对实时性要求并不高。因此,我们可以将它们异步化处理,即通过后台线程或子进程的方式执行这些耗时任务,从而避免阻塞主线程,提升 Redis 的响应能力和吞吐量。
- 持久化操作:如 RDB 快照生成、AOF 重写,通过 fork 子进程完成,主线程不被阻塞。
- 惰性删除(Lazy Free) :使用
UNLINK
命令删除大对象时,实际内存释放由后台线程异步完成。 - 过期键清理:Redis 采用惰性删除与定期删除相结合的方式,部分清理任务异步执行。
- 主从复制:写命令的同步是异步进行的,提升主节点的响应效率。
Redis 的多线程
Redis 的多线程架构在使用方面上,主要还是在IO处理上面。首先,在 Redis 早期完全单线程的结构中,面对IO瓶颈是通过多路复用的方式解决的。
IO多路复用 一种同步的IO模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪,就能通知到对应程序进行相应的读写操作,没有文件句柄就绪时,就会阻塞应用程序,从而释放CPU资源。
我们来介绍几个概念:
- I/O
- 网络I/O,尤其在操作系统层面指数据在内核态和用户态之间的读写操作
- 多路
- 多个客户端连接(连接就是套接字描述符,即socket或者channel)
- 复用
- 复用一个或者几个线程连接
- IO多路复用
- 也就是说一个或者一组线程处理多个TCP,使用单进程就能实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程
总结
- 一个服务端进程可以同时处理多个套接字描述符
- 实现多路复用的模型有3种:可以分select->poll->epoll3个阶段来描述
但是随着 Redis QPS增多,一个线程同时处理的IO请求变得越来越多。举个极端的例子,当一个线程通过IO多路复用,同时监控1k个socket接口,这时候瓶颈反而到了CPU上面。因此,我们就需要映入多个线程来进行负载均衡,但是依然是一个线程通过IO多路复用对多个socket。
总结
Redis 的多线程设计主要致力于解决网络 I/O 在高并发下的性能瓶颈 ,而非为了并行执行 Redis 命令。它采用了主线程 + 多个 I/O 子线程的混合模型,既保留了 Redis 的一致性优势,又显著提升了整体吞吐能力。
- 主线程负责接收客户端连接、解析命令以及执行核心逻辑操作,确保数据的一致性和命令执行的原子性。
- I/O 子线程用于加速网络请求的读取和响应写回过程,特别是在高并发场景下,通过多个I/O子线程来均衡负载,提高处理效率。
与此同时,Redis 还引入了异步处理机制以应对那些相对耗时但对实时性要求不高的任务,例如持久化、空间清理和删除大对象(Big Key)。这些操作如果同步执行会阻塞主线程,影响系统性能。因此,Redis 采用以下策略进行优化:
- 持久化操作:如 RDB 快照生成和 AOF 文件重写,通过 fork 子进程完成,避免阻塞主线程。
- 惰性删除(Lazy Free) :使用
UNLINK
命令删除大对象时,实际内存释放由后台线程异步完成。 - 过期键清理:Redis 结合惰性删除与定期删除的方式,部分清理任务异步执行,减少对主线程的影响。
综上所述,Redis 不仅通过多线程架构提升网络 I/O 性能,还利用异步处理机制优化了耗时任务的管理。这种综合设计使得 Redis 能够在保持高性能的同时,提供稳定可靠的服务。