03 | 高性能IO模型:为什么单线程Redis能那么快?
“为什么单线程的 Redis 能那么快?”
通常说 Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
所以,严格来说,Redis 并不是单线程,但是我们一般把 Redis 称为单线程高性能,这样显得“酷”些。
“为什么用单线程?为什么单线程能这么快?”
需要深入地学习下 Redis
的单线程设计机制以及多路复用机制。
Redis 为什么用单线程?
要先了解多线程的开销。
的确,对于一个多线程的系统来说,在有合理的资源分配的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。下面的左图是我们采用多线程时所期待的结果。
但是,通常情况下,在我们采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。
一个关键的瓶颈在于,系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。
并发访问控制一直是多线程开发中的一个难点问题。
如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。
而且,采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis
直接采用了单线程模式。
单线程 Redis 为什么那么快?
通常来说,单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力,这是为什么呢?
其实,这是 Redis
多方面设计选择的一个综合结果。
一方面,Redis 的大部分操作在内存
上完成,再加上它采用了高效的数据结构,例如哈希表和跳表
,这是它实现高性能的一个重要原因。
另一方面,就是 Redis 采用了多路复用机制
,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
基本 IO 模型与阻塞点
其中,bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get
属于键值数据操作。
既然 Redis 是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。
Redis基本IO模型
在这里的网络 IO 操作中,有潜在的阻塞点,分别是 accept()
和 **recv()
**。
当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。
这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。
不过,幸运的是,socket
网络模型本身支持非阻塞模式。
非阻塞模式
Socket 网络模型的非阻塞模式设置,主要体现在三个关键的函数调用上。
在 socket 模型中,不同操作调用后会返回不同的套接字类型。
socket()
方法会返回主动套接字,然后调用 listen()
方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。最后,调用 accept()
方法接收到达的客户端连接,并返回已连接套接字。
Redis套接字类型与非阻塞设置
基于多路复用的高性能 I/O 模型
指一个线程处理多个 IO
流,就是我们经常听到的 select/epoll
机制。
简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
基于多路复用的Redis高性能IO模型
图中的多个 FD 就是刚才所说的多个套接字。
Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。
为了在请求到达时能通知到 Redis 线程,select/epoll
提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
其实,select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。
这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于 Redis 单线程),效率也能提升。
小结
今天,我们重点学习了 Redis 线程的三个问题:
- “Redis 真的只有单线程吗?”
- “为什么用单线程?”
- “单线程为什么这么快?”