Redis 是一个高性能的、基于内存的键值数据库。根据官方测试报告,它在单台机器上可以支持大约 100,000 QPS(每秒查询数)。然而,Redis 的设计采用了单线程架构。
为什么 Redis 在单线程设计下依然有如此高的性能?使用多线程处理并发请求不是更好吗?
在本文中,我们将探讨为什么 Redis 采用单线程架构并且仍能保持高速。重点在以下四个方面:
内存中的数据存储
高效的数据结构
单线程架构
非阻塞 I/O
接下来我们逐一进行分析。
内存中的数据存储Redis 完全基于内存,数据存储在内存中。绝大部分请求都是纯内存操作,速度极快。与传统的磁盘文件数据存储相比,Redis 避免了通过磁盘 I/O 从磁盘读取数据到内存的开销。
高效的数据结构Redis 有 5 种数据类型:字符串、列表、哈希、集合 和 有序集合。
不同的数据类型在底层使用一种或多种数据结构进行支持,目的是为了实现更快的速度。
单线程架构使用单线程可以节省大量的上下文切换时间和 CPU 消耗,不存在竞争条件,也无需考虑各种锁定问题,没有因死锁等原因导致的锁定和解锁操作带来的性能开销。此外,还可以使用各种“线程不安全”的命令,比如 Lpush。
需要注意的是,当我们强调单线程时,指的是使用一个线程来处理网络 I/O 和键值对的读写(文件事件分发器)。换句话说,一个线程处理所有的网络请求,但 Redis 的其他功能,如持久化、异步删除和集群数据同步,实际上是由额外的线程执行的。
那么为什么要使用单线程呢?官方答案是,因为 CPU 不是 Redis 的瓶颈,瓶颈更可能是机器的内存或网络带宽。由于单线程实现简单,且 CPU 不会成为瓶颈,因此采用单线程解决方案是合理的。
尽管多线程架构可以通过上下文切换使应用程序并发处理任务,但对于 Redis 来说,提升的性能有限,因为大多数线程最终都会因网络 I/O 而阻塞。
还要注意,因为 Redis 使用单线程,如果某个命令执行时间过长(如 hgetall 命令),可能会导致阻塞。Redis 作为一个内存数据库,设计的初衷是为了快速执行,因此在使用 lrange、smembers、hgetall 等命令时需要谨慎。
非阻塞 I/O基于网络 I/O 多路复用(非阻塞 I/O)的线程模型可以处理并发连接,并有助于缓解网络 I/O 速度缓慢的问题。
多路复用 I/O 模型利用了 select、poll 和 epoll 同时监控多个流的 I/O 事件的能力。当线程处于空闲状态时会被阻塞,当一个或多个流有 I/O 事件时,线程从阻塞状态中被唤醒,程序会轮询所有流(epoll 只轮询出现事件的流),然后依次处理准备好的流。这样可以避免大量无意义的操作。
这里的“多路”指的是多个网络连接,“复用”指的是复用同一个线程。使用多路复用 I/O 技术允许单个线程高效地处理多个客户端的网络 I/O 连接请求(以最小化在网络 I/O 上消耗的时间)。
Redis 的网络事件处理器基于 Reactor 模型,也被称为文件事件处理器。
文件事件处理器使用 I/O 多路复用同时监听多个套接字,并将套接字执行的任务与不同的事件处理器关联。
文件事件在单线程模式下运行,但通过使用 I/O 多路复用程序监听多个套接字,文件事件处理器实现了高性能的网络通信模型。
Redis 通过单个顺序的主线程处理客户端请求,包括接收(套接字读取)、解析、执行和发送(套接字写入),这就是所谓的单线程模式。
多个套接字可能生成不同的操作,每个操作对应不同的文件事件。然而,I/O 多路复用程序监听多个套接字,并将套接字生成的事件排入队列。事件分发器每次从队列中取出一个事件,并将该事件传递给相应的事件处理器进行处理。
Redis 客户端到服务器的调用经历三个过程:发送命令、执行命令和返回结果。在命令执行阶段,由于 Redis 在处理命令时是单线程的,服务器收到的每个命令不会立即执行。所有命令会被放入队列中,按顺序一个一个执行。多个客户端发送的命令执行顺序是不确定的,但可以肯定的是,两条命令不会同时执行,从而避免了并发问题。这就是 Redis 的基本单线程模型。
Redis 6.0 的多线程解析为什么 Redis 6.0 之前没有使用多线程?Redis 采用单线程的方式来实现高可维护性。虽然多线程在某些方面可能表现良好,但多线程会引入程序执行顺序的不确定性,导致一系列并发读写问题。这增加了系统的复杂性,并且可能因线程切换、锁定和解锁而导致性能损失,甚至出现死锁问题。
为什么 Redis 6.0 引入了多线程?Redis 6.0 引入了多线程,因为其瓶颈不在内存,而是在网络 I/O 模块上,该模块消耗了大量 CPU 时间。因此,引入多线程用于处理网络 I/O,以充分利用 CPU 资源,减少网络 I/O 阻塞导致的性能损失。
如何在 Redis 6.0 中启用多线程?默认情况下,Redis 中多线程是禁用的,可以在配置文件中启用:
io-threads-do-reads yesio-threads [number of threads]
官方建议的线程数是:对于四核机器设置 2-3 个线程,八核机器设置 6 个线程。线程数应小于机器核心数,且最好不要超过 8 个线程。
多线程模式下会有线程并发问题吗?如图所示,Redis 请求涉及建立连接、获取要执行的命令、执行命令,最后将响应写入套接字。
在 Redis 的多线程模式中,接收、发送和解析命令可以配置为在多个线程中执行,因为这些是我们识别出的主要耗时点。然而,涉及内存操作的命令执行仍然在单线程中进行。
因此,Redis 的多线程部分仅用于处理网络数据的读写和协议解析。命令执行仍然在单线程中顺序执行,因此不存在并发安全问题。