Redis线程模型
前面的文章介绍了Redis的底层数据结构,这篇文章来介绍一下Redis的线程模型。
Redis为什么选择单线程?
官方的回答是这样的,对于Redis来说,CPU通常不会成为瓶颈,因为大多数的请求不会是CPU密集型的,而是IO密集型的。如果不考虑RDB、AOF等信息持久化方案的话,Redis是完全纯内存操作,纯内存操作的执行速度是非常快的,所以这部分操作不会成为Redis的性能瓶颈。真正影响Redis性能的,是网络IO,也就是客户端和服务端之间的网络延迟造成的。因此Redis采用了单线程,在网络IO部分采用IO多路复用。
更加具体的原因是在CPU不是性能瓶颈的前提下,相对于多线程模型,单线程模型避免了频繁上下文切换造成的开销,同时也不用考虑Redis众多底层数据结构的并发安全问题,这也使得Redis的底层相对简单好维护。总的来说,Redis采用单线程可以在保持高性能的同时,保持代码的简单可维护,毕竟多线程的理论和实际对比就可以很好的说明多线程在使用时的复杂与难以维护:
Redis为什么又引入了多线程?
Redis其实在v4.0时就引入了多线程,主要用来异步处理一些比较耗时的任务,避免核心线程被长时间阻塞;v6.0时引入了多线程IO,主要用来处理网络数据的读写和协议内容的解析。多线程IO的引入主要是由于互联网的飞速发展,互联网业务需要处理的线上流量越来越大,Redis的单线程模式会导致相当一部分CPU时间消耗在网络IO上,而导致Redis整体的吞吐量降低。如果要对Redis性能进行优化,可以从两方面来入手:①优化网络IO模块、②提高机器内存的读写速度。
第②种依赖于硬件的突破,所以只能从第一种方法入手,第一种方法又可以细分为两个方向:①零拷贝技术或者DPDK技术、②利用多核CPU优势。
零拷贝技术有自身的局限性,无法完全适配Redis这一类复杂的网络IO场景。DPDK技术通过旁路网卡IO绕过内核协议的方式又太过于复杂以及需要内核的支持。所以最终选择的就是利用多核CPU的优势,通过使用多线程将网络IO任务进行分摊。
Redis6.0在引入多线程IO后,在4线程IO时,GET/SET的性能相较于单线程几乎翻倍。
Redis的线程模型
Redis的线程模型分为v6.0之前和v6.0之后两个版本。
v6.0之前:
采用基于Recator模式的网络事件处理器。这个处理器被称为文件事件处理器。它的组成结构为4部分,多个Socket、IO多路复用程序、文件事件分派器和事件处理器。因为文件事件分派器队列和消费是单线程的,所以说Redis是单线程模型。Redis6.0之前的线程模型如下图所示:
多个Socket:Socket会产生AE_READABLE和AE_WRITABLE事件。当Socket变得可读或者有新的可以应答的Socket出现时,就会产生一个AE_READABLE事件;当Socket变得可写时,就会产生一个AE_WRITABLE事件。
IO多路复用程序:IO多路复用的实现有select、poll和epoll三种技术,其中epoll是性能最好的。
事件处理器:事件处理器包括连接应答处理器、命令请求处理器和命令回复处理器。如果是客户端请求连接Redis,那么会为Socket关联连接应答处理器;如果是客户端要写数据到Redis,那么会为Socket关联命令请求处理器;如果是客户端要从Redis读数据,那么会为Socket关联命令回复处理器。
多个Socket会产生不同的事件,不同的事件对应着不同的操作,IO多路复用程序监听着这些Socket,当这些Socket产生了事件,IO多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件事件分派器中传送。当事件处理器处理完一个事件后,IO多路复用程序才会继续向文件分派处理器传送下一个事件。
Redisv6.0线程模型下,客户端与Redis一次完整的通信流程如下:
①Redis启动初始化时,Redis会将连接应答处理器与AE_READABLE事件关联起来。
②一个客户端发起与Redis的连接请求,此时Redis产生一个AE_READABLE事件,此时连接应答处理器来处理此事件,连接应答处理器与客户端建立连接,创建客户端相应的Socket,同时将这个Socket的AE_READABLE事件与命令请求处理器关联。
③这个客户端向Redis发送了一条命令,相应的Socket产生一个AE_READABLE事件,IO多路复用程序将事件压入队列中。事件分配处理器拿到该事件,将该事件分配给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。完成后将Socket的AE_WRITABLE事件与命令回复处理器相关联。
④如果客户端已经准备好接收数据,相应Socket会产生一个AE_WRITABLE事件,同时会压入队列中然后被事件分配处理器分配给命令回复处理器,由其将准备好的响应数据写入Socket给客户端读取。
⑤命令回复处理器写完后,就会删除该Socket的AE_WRITABLE事件与命令请求处理器的关联关系。
v6.0之后:
v6.0之后的流程与v6.0之前的最大区别在于网络数据的读写部分。
Redis的主线程负责接受连接建立请求,获取Socket并将其放入全局等待读处理队列;主线程处理完读事件后,通过Round Robin将这些链接分配给IO线程;主线程阻塞等待IO线程读取Socket完成;主线程通过单线程的方式执行请求命令,并将数据写入缓冲区;主线程阻塞等待IO线程将数据写回给Socket。具体流程如下图所示:
可以看到,Redis的多线程只体现在Socket命令的读取和写回,而命令的执行还是主线程在单线程执行,所以说Redis的核心仍然是单线程的,不用考虑并发安全的问题。
到此Redis的线程模型就介绍完毕啦,大家有什么问题或者勘误可以在评论区留言,笔者看到都会回复的。