Redis中的多路复用模型
Redis6用到了多线程?那多线程应用在哪些地方,引入多线程后,又改如何保证线程安全性呢?
同时,如何在性能和线程安全性方面做好平衡?
在Redis6.0之前,我们一直说Redis是单线程,所以并不会存在线程安全问题,而这个单线程,实际上就是在做数据IO处理中,是用的主线程来串行执行,如图4-7所示。
Redis基于Reactor模式设计开发了自己的一套高效事件处理模型,这个事件处理模型对应的就是Redis中的文件事件处理器,这个文件事件处理器是单线程运行的,这也是为什么我们一直强调Redis是线程安全的。
既然Redis是基于Reactor模型实现,那它必然用了I/O多路复用机制来监听多个客户端连接,然后把感兴趣的事件(READ/ACCEPT/CLOSE/WRITE)注册到多路复用器中。
文件事件处理器中使用I/O多路复用模型同时监听多个客户端连接,并且根据当前连接执行的任务类型关联不同的事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)来处理这些事件。
这样设计的好处:
文件事件处理器实现了高性能的网络IO通信模型
通过单线程的方式执行指令,避免同步机制的性能开销、避免过多的上下文切换、整体实现比较简单,不需要考虑多线程场景中的各种数据结构的线程安全问题。
图4-7其实严格意义上来说,在Redis4.x版本就支持了多线程,只是,负责客户端请求的IO处理使用的是单线程。但是针对那些非常耗时的命令,Redis4.x提供了异步化的指令来处理,避免因为IO时间过长影响到客户端请求IO处理的线程。比如在 Redis v4.0 之后增加了一些的非阻塞命令如 UNLINK(del命令的异步版本)、FLUSHALL ASYNC、FLUSHDB ASYNC。
Redis6.0之后的多线程?在Redis6.0中引入了多线程,可能很多同学会误以为redis原本的单线程数据IO变成了多线程IO,那作者不就是在打自己的脸吗?
对于Redis来说,CPU通常不是瓶颈,因为大多数请求不是属于CPU密集型,而是I/O密集型。而在Redis中除了数据的持久化方案之外,它是完全的纯内存操作,因此执行速度是非常快的,所以数据的IO并不是Redis的性能瓶颈,Redis真正的性能瓶颈是在网络I/O,也就是客户端和服务端之间的网络传输延迟,所以Redis选择了单线程的IO多路复用来实现它的核心网络模型。
前面我们说过,单线程设计对于Redis来说有很多好处。
避免过多的上上下文切换开销
避免同步机制的开销,涉及到数据同步和事务操作时,避免多线程影响所以必然需要加同步机制保证线程安全性。但是加锁同时也会影响到程序的执行性能。
维护简单,引入多线程之后,不管是对数据结构的设计,还是在程序代码的维护上,都会变得很复杂。
所以既然Redis的数据I/O不是瓶颈,同时单线程又有这么多好处,那Redis自然就采用单线程了。既然是这样,那么Redis 6.0引入多线程,一定不是优化数据IO性能,那么我们先来分析一下Redis性能瓶颈主要体现在哪些方面,无非就是三个方面。
网络IO
CPU核心数
内存
由于CPU核心数并不是redis的瓶颈,所以影响Redis性能的因素只有网络IO和内存,而内存属于硬件范畴,比如采用容量更大、吞吐量更高的内存进行优化就行,因此也不是属于Redis可优化的空间,所以最终我们发现Redis的性能瓶颈还是在网络IO上。
而在Redis6.0之前,使用的是单线程Reactor模型,单线程模型是指对于客户端的请求,主线程需要负责对这个请求的完整IO过程进行处理,如图4-8所示,从socket中读取数据和往socket中写数据都是比较耗时的网络IO操作,解析请求和内存交互耗时可能远小于这个网络IO操作。
图4-8按照前面我们对多Reactor多线程的理解,那我们能不能改成主从多Reactor多线程模型呢?主Reactor负责接收客户端连接,然后分发给多个Reactor进行网络IO操作。很显然,这样做就会导致Redis编程了一个多线程模型,这对Redis的影响较大,因为多线程带来的线程安全问题和底层复杂的数据结构的操作都非常棘手,所以Redis 6.0并没有这么做。