前言

image.png

相信大家已经听过无数遍“Redis是单线程的”这句话了,Redis真的是单线程的吗,又是如何支撑那么大的并发量,并且运用到了这么多的互联网应用中的呢?

 

其实,Redis的单线程指的是Redis内部会有一个主处理线程,充分利用了非阻塞、IO多路复用模型,实现的一个Reactor架构。但是在某些情况下,Redis会生成线程或者子进程来执行某些比较繁重的任务。

 

Redis包含了一个称为AE事件模型的强大的异步事件库来包装不同操作系统的IO复用技术,如epoll、kqueue、select等。

一、Redis线程模型

还是那个Reactor模型,只不过我们再次踏入了不同的国界,于是又出现了一种新的的表述方式。

Redis基于Reactor模型开发了网络事件处理器,这个处理器被称为文件事件处理器。不过叫什么不重要,重要的是原理都是一样的。以下是Redis的线程模型: image.png

这个图基本上涵盖了Redis进程处理的主要事情:

  • 客户端A发起请求建立连接,监听套接字Server Socket建立连接之后,产生一个AE_READABLE事件;
  • 该事件被IO多路复用处理,放入事件队列,最终被文件事件分派器分派给了连接应答处理器进行处理:
    • 连接应答处理器处理新连接,将FD1套接字的AE_READABLE事件与命令请求处理器关联起来。
  • 客户端A最终在客户端生成一个已连接套接字FD1;
  • 客户端A发送命令请求,产生一个AE_READABLE事件,该事件被IO多路复用处理,放入事件队列,最终被文件事件分派器分派给了命令请求处理器进行处理:
    • 命令请求处理器执行客户端FD1套接字命令操作,得到结果,将结果写入到套接字的回复缓冲区中,准备好响应给客户端;
    • 同时将FD1套接字的AE_WRITABLE事件与命令回复处理器关联;
  • 当FD1套接字准备好写的时候,会产生一个AE_WRITABLE事件,该事件被IO多路复用处理,放入事件队列,最终被事件分派器分派给了命令回复处理器进行处理:
    • 命令回复处理器把结果输出响应给客户端的FD1已连接套接字;
    • 然后将FD1套接字的AE_WRITABLE事件跟命令回复处理器解除关联。

大致一个交互流程就这样完成了,是不是很简单呢。

 

这个Reactor模型没有哪个服务器程序实现是最好的一说,但是对于我来说,你的点赞,关注,评论,转发就是最好的支持。

二、为啥Redis单线程也这么高效?

前面已经讲了这么多Reactor模式的好处,详细大家心里也有个底了,大致总结下:

  • Redis是存内存操作的,所以处理速度非常快,这同时也跟Redis高效的数据结构有关,不过本文重点是讲网络相关的,数据结构不展开讲;
  • Redis的瓶颈不在CPU,而是在内存和网络。而基于Reactor模型,实现了非阻塞的IO多路复用,尽可能高效的利用了CPU,避免不必要的阻塞;
  • 单线程反而避免了上下文切换的开销。

对于开发人员来说,最关注的一点就是:单线程降低了开发的复杂度,再也不需要处理各种竞态条件了,就连Hash的惰性Rehash,Lpush等线程不安全的命令都可以进行无锁编程了。

三、Redis真的是单线程的吗?

我再问一句大家,Redis真的是单线程的吗,从Reactor模型上来说,单线程肯定会存在瓶颈的。

 

比如 UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC等非阻塞的删除操作,如果要释放的内存空间比较大,就需要耗费比较多的时间进行处理,这些操作就会阻塞住待处理的线程。而如果是单线程模型,可想而知,整个Redis服务的请求都会被阻塞住了;

 

为此,Redis引入了多线程机制。

REDIS 4.0初步引入多线程

在Redis 4.0中,Redis开始使具有更多线程。这个版本仅限于在后台删除对象,其中包括非阻塞的删除操作。

UNLINK操作,只会将键从元数据中删除,并不会立刻删除数据,真正的删除操作会在一个后台线程异步执行。

REDIS 6.0真正引入多线程

虽然基于Reactor模型,单线程也可以支持很大的并发量,但是要是IO读写多了,待处理的已连接套接字多了,需要执行的命令也多了,那么,单线程依旧是瓶颈,这个时候我们就要引入主从Reactor模型,甚至主从Reactor模型+Worker线程池了。

在Redis 6.0中,如果要开启多线程,可以进行设置:

io-threads 线程数 io-threads-do-reads yes // 默认IO线程只会用于写操作,如果要在读操作和协议解析的时候启用IO线程,则可以设置该选项为yes,但是Redis团队声称它并没有多大帮忙

不过呢,Redis为了避免产生线程并发安全的问题,在执行命令阶段仍然是单线程顺序执行的,只是在网络数据读写和协议解析阶段才用到了多线程。

 

为了进一步了解这个特性,我们可以阅读以下 redis.conf[11] 配置文件的说明。在这里,这个特性被命名为:THREADED I/O,下面是翻译整理自里面的一些说明。

THREADED I/O

Redis大多是单线程的,但是有一些线程操作,例如UNLINK,执行缓慢的I/O访问等是在后台线程上执行的操作。

 

但是现在,可以在不同的I/O线程中处理Redis客户端套接字的读写。 由于特别慢的写入速度,因此Redis用户通常使用pipelining流水线以加快每个内核的Redis性能,并生成多个实例以扩大规模。 而使用I/O线程可以轻松地将Redis写入速度提升2倍。

 

IO线程默认是禁用的,建议在至少有4个内核的机器上启用,至少保留一个备用内核。也就是说,如果您的CPU是4核的,请尝试使用2~3个IO线程。

io-threads 4

将io-threads设置为1只会像传统一样只启用单线程。

使用8个以上的线程不会有太大帮助,并且建议实际存在性能问题的时候才使用IO线程,否则就没有必要使用了。

 

启用IO线程后,我们仅将IO线程用于写操作,即对write(2)系统调用进行线程化并将客户端缓冲区传输到套接字。 但是,也可以使用以下配置指令通过以下方式启用读取线程和协议解析:

io-threads-do-reads yes

通常,线程读取没有太大帮助。

Redis用的是类似单线程版的Reactor + IO线程池(Worker线程池),不过与我们前面提到的单线程Reactor + Worker线程池模式有所不同,再回顾下Reactor + Worker线程池模式: image.png

Redis是在所谓的Reactor线程(主线程)中把IO读事件一批一批的交给IO线程池进行读取,读取完毕之后,统一执行所有请求的命令,然后才是一次性把所有请求的响应写到socket,如下图所示: image.png

等待队列中的待处理时间平均分给每个IO线程,IO线程池只是负责IO读写和解析数据,IO线程池充分利用了CPU多核处理的能力,提高了IO读写速度。所以,我们再来强调一次重点:Redis为了避免产生线程并发安全的问题,在执行命令阶段仍然是单线程顺序执行的,只是在网络数据读写和协议解析阶段才用到了多线程。

REDIS 6.0真的是单线程的吗?

可以发现,Redis也不是简单粗暴的引入多线程机制,而是基于避免引入并发操作的复杂度的前提下,进行的合理改良设计实现的。

如果真有人要跟你追究Redis是不是单线程的,你要记住的是,即使是Redis 6.0,执行命令的过程仍然是单线程的。

 

参考: https://www.itzhai.com/articles/redis-technology-insider-cache-data-structure-concurrency-clustering-and-algorithm.html

https://www.itzhai.com/articles/decrypt-the-threading-model-of-common-server-programs.html

https://blog.csdn.net/weixin_44479862/article/details/126512100