单线程 Redis 为何如此之快

291 阅读6分钟

Redis是一个高性能的内存键值数据库。根据官方测试报告,它在单机上可支持约10万QPS(每秒查询次数)。然而,Redis在设计上采用的是单线程架构。

为什么Redis采用单线程设计仍有如此高的性能?使用多线程进行并发请求处理不是更好吗?

在本文中,让我们来探讨Redis为何采用单线程架构却仍能保持非常快的速度。重点关注以下四个方面:

  1. 内存中的数据存储

  2. 高效的数据结构

  3. 单线程架构

  4. 非阻塞I/O

下面让我们详细分析每个方面。

使用内存存储数据

Redis完全基于内存,数据存储在内存中。绝大多数请求都是纯内存操作,速度极快。与传统的磁盘文件数据存储相比,Redis避免了通过磁盘I/O将数据从磁盘读入内存的开销。

高效的数据结构

Redis共有5种数据类型:字符串(String)、列表(List)、哈希(Hash)、集合(Set)和有序集合(SortedSet)。

不同的数据类型在底层使用一种或多种数据结构来支持,目的是实现更快的速度。

单线程架构

使用单线程节省了大量的上下文切换时间和CPU消耗,不存在竞争条件,无需考虑各种锁问题,也没有因死锁可能导致性能开销的加锁和解锁操作。此外,它允许使用各种“线程不安全”的命令,如 Lpush

需要注意的是,当我们强调单线程时,是指使用一个线程来处理网络I/O和键值对读写(文件事件分派器)。换句话说,一个线程处理所有网络请求,但Redis的其他功能,如持久化、异步删除和集群数据同步,实际上是由额外的线程执行的。

那么为什么使用单线程呢?官方的答案是,因为CPU不是Redis的瓶颈,最有可能的瓶颈是机器内存或网络带宽。由于单线程易于实现且CPU不会成为瓶颈,采用单线程解决方案是有意义的。

虽然多线程架构允许应用程序通过上下文切换并发处理任务,但它对Redis的性能提升很小,因为大多数线程最终会被网络I/O阻塞。

同样需要注意的是,由于Redis使用单线程,如果一个命令执行时间过长(如 hgetall 命令),可能会导致阻塞。Redis是一个为快速执行而设计的内存数据库,所以在使用 lrangesmembershgetall 等命令时要谨慎。

非阻塞I/O

使用基于网络I/O多路复用(非阻塞I/O)的线程模型,可以处理并发连接,并有助于缓解网络I/O速度慢的问题。

多路复用I/O模型利用了 selectpollepoll 同时监视多个流的I/O事件的能力。空闲时,当前线程被阻塞。当一个或多个流有I/O事件时,线程从阻塞状态唤醒,程序轮询所有流(epoll 只轮询产生事件的流),然后依次处理就绪的流。这种方法避免了大量无用操作。

这里,“多路复用”指的是多个网络连接,“复用”指的是复用同一个线程。使用多路复用I/O技术允许一个单线程高效处理多个客户端网络I/O连接请求(以最小化在网络I/O上花费的时间)。

Redis的网络事件处理程序基于反应器模式,也称为文件事件处理程序。

文件事件处理程序使用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 yes 
io-threads [线程数量]

根据官方指南,推荐的线程数量是对于4核机器设置2 - 3个线程,对于8核机器设置6个线程。线程数量应小于机器核心数,并且尽可能不超过8个线程。

多线程模式下是否存在线程并发问题?

如图所示,一个Redis请求涉及建立连接、获取要执行的命令、执行命令,最后将响应写入套接字。

在Redis的多线程模式下,接收、发送和解析命令可以配置为在多个线程中执行,因为它们是我们确定的主要耗时点。然而,涉及内存操作的命令执行仍然在单线程中运行。

因此,Redis的多线程部分仅用于处理网络数据读写和协议解析。命令执行仍然按顺序在单线程中执行,所以不存在并发安全问题。