我们都知道,Redis使用单线程模型,可以达到10K级别的并发量。单线程为什么可以这么扛这么高的并发呢?我们一起探讨一下。
什么是 Redis 单线程?
Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
为什么 Redis 用单线程?
与单线程相对应的自然就是多线程,我们都知道使用多线程可以提升程序的性能。我们所谓的提升性能主要是指降低延迟,提高吞吐量。那么多线程是怎样提高性能的呢?靠的是提升硬件利用率,在详细一点就是提高IO利用率和CPU利用率。由于CPU的速度和IO设备的速度相差好多个数量级,所以我们使用多线程就可以协调两者的速度差异,从而提高整体硬件的利用率。
但是Redis 如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作。所以CPU不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本。
整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O。网络 I/O 可以通过IO多路复用解决。
具体Redis是怎样实现IO多路复用的可以参考最后给的链接。
Redis 6.0中指的多线程又是什么?
随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。毕竟一个线程只能绑定到一个核心上,无法发挥多核的优势。
为了应对这个问题,一般有两种方法。
第一种方法是,用用户态网络协议栈(例如 DPDK)取代内核网络协议栈,让网络请求的处理不用在内核里执行,直接在用户态完成处理就行。这个方法要求在 Redis 的整体架构中,添加对用户态网络协议栈的支持,需要修改 Redis 源码中和网络相关的部分(例如修改所有的网络收发请求函数),这会带来很多开发工作量。而且新增代码还可能引入新 Bug,导致系统不稳定。所以,Redis 6.0 中并没有采用这个方法。
第二种方法就是采用多个 IO 线程来处理网络请求,提高网络请求处理的并行度。充分利用多核。但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。在Redis 6.0中默认是不开启的。
我们来看下,在 Redis 6.0 中,主线程和 IO 线程具体是怎么协作完成请求处理的。为了方便你理解,我们可以把主线程和多 IO 线程的协作分成四个阶段。
阶段一:服务端和客户端建立 Socket 连接,并分配处理线程
首先,主线程负责接收建立连接请求。当有客户端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接,并把 Socket 放入全局等待队列中。紧接着,主线程通过轮询方法把 Socket 连接分配给 IO 线程。
阶段二:IO 线程读取并解析请求
主线程一旦把 Socket 分配给 IO 线程,就会进入阻塞状态,等待 IO 线程完成客户端请求读取和解析。因为有多个 IO 线程在并行处理,所以,这个过程很快就可以完成。
阶段三:主线程执行请求操作
等到 IO 线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。下面这张图显示了刚才介绍的这三个阶段,你可以看下,加深理解。
阶段四:IO 线程回写 Socket 和主线程清空全局队列
当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。
和 IO 线程读取和解析请求一样,IO 线程回写 Socket 时,也是有多个线程在并发执行,所以回写 Socket 的速度也很快。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。
我也画了一张图,展示了这个阶段主线程和 IO 线程的操作,你可以看下。