Redis 是单线程是指 Redis 的网络 IO和键值对读写是由一个线程来完成的。但 Redis 的持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。所以严格来说Redis 并不是单线程的。
Redis 为什么用单线程,就要先了解多线程的开销。
使用多线程,可以增加系统吞吐率、增加系统扩展性。在有合理的资源分配的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。下面的左图是采用多线程时的理想结果。
但是通常情况下采用多线程后,如果没有良好的系统设计,实际得到的结果是右图。刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。
下降原因是:
为了避免这些问题,Redis 直接采用了单线程模式。
通常来说,单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力,这是因为Redis 多方面设计选择的一个综合结果。
网络操作的基本 IO 模型和潜在的阻塞点。以 Get 请求为例,需要监听客户端请求(bind/listen),和客户端建立连接(accept),从 socket 中读取请求(recv),解析客户端发送请求(parse),根据请求类型读取键值数据(get),最后给客户端返回结果,即向 socket 中写回数据(send)。
如下图,其中bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get 属于键值数据操作。既然 Redis 是单线程,那么最基本的一种实现是在一个线程中依次执行上面说的这些操作。
这里的网络 IO 操作中,有潜在的阻塞点,分别是 accept() 和 recv():
这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。不过 socket 网络模型本身支持非阻塞模式。
Socket 网络模型的非阻塞模式设置,主要体现在socket()、listen()、 accept() 三个关键的函数调用上。
在 socket 模型中,不同操作调用后会返回不同的套接字类型。socket() 方法会返回主动套接字,然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。
针对监听套接字,可以设置非阻塞模式:当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。但是,你要注意的是,调用 accept() 时,已经存在监听套接字了。
虽然 Redis 线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求,并在有请求时通知 Redis。
针对已连接套接字,也可以设置非阻塞模式:Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,Redis 线程同样可以返回处理其他操作。也需要有机制继续监听该已连接套接字,并在有数据达到时通知 Redis。
这样才能保证 Redis 线程,既不会像基本 IO 模型中一直在阻塞点等待,也不会导致 Redis 无法处理实际到达的连接请求或数据。
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,即 select/epoll 机制。
在 Redis 只运行单线程的情况下, 多路复用机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
图中的多个 FD(文件描述符) 就是刚才所说的多个套接字。 Redis 网络框架调用 epoll 机制,让Linux内核监听这些套接字。此时Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,不会阻塞在某一个特定的客户端请求处理上。所以Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。
select/epoll 提供了基于事件的回调机制,针对不同事件的发生,调用相应的处理函数。select/epoll 监测到 FD 上有请求到达时,就会触发相应的事件。
这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。好处:
因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。
例如现在有两个请求,这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件 和 Read 事件,此时内核就会回调 Redis 相应的 accept 和 get 函数进行处理。
这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于 Redis 单线程),效率也能提升。
事件的回调机制的实现有很多种:
可以根据 Redis 实际运行的操作系统,选择相应的多路复用实现。
Redis 单线程是指网络 IO 和数据读写的操作采用了一个线程,而采用单线程的一个核心原因是避免多线程开发的并发控制问题。
单线程的 Redis 也能获得高性能,跟多路复用的 IO 模型密切相关,因为这避免了 accept() 和 send()/recv() 潜在的网络 IO 操作阻塞点。
Redis 6.0 中提出了多线程模型。