我们先给出结论,为什么Redis单机QPS能够达到10W?
- Redis大部分请求是基于内存的;
- Redis拥有简单高效的数据结构;
- Redis是基于单线程的IO多路复用的事件机制;
对上述三大原因进行逐条分析:
Redis大部分请求都是基于内存操作:
- 我知道内存和磁盘的读写速度完全不是一个量级的。Redis启动时便会将持久化文件中的数据加载到内存中,而传统关系型数据库对于新的查询需要经历磁盘IO的时间消耗。
Redis拥有简单高效的数据结构:
- Redis拥有SDS,链表,字典,跳跃表,整数集合,压缩列表等简单的数据结构,这使得CPU处理这些数据结构向上封装的数据类型会十分的快速;
Redis是基于单线程的IO多路复用的事件机制: (重点)
- 单线程 、IO多路复用的事件机制
单线程:
Redis的单线程指的是网络IO处理和Key-Value数据信息处理共用一个线程;
为什么Redis使用单线程:
我们都知道多线程可以提升程序的吞吐率,提高程序的并发程度;
通常情况下,在我们采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。
为什么会出现这种情况呢?一个关键的瓶颈在于,程序中通常存在被多线程同时访问的共享资源,比如一个共享的数据变量。当有多个线程要修改这个共享变量时,为了保证共享变量的正确性,就需要有额外的机制进行保证(比如:加锁),而这个额外的机制,就会带来额外的系统开销。
总体来说Redis基于内存读取数据,基于简单数据结构处理数据,这些都是十分快速的操作。而引入多进程可能导致出现额外开销时间 多线程处理简单数据时间 > 单线程处理简单数据的时间,显然是得不偿失的。因而Redis的设计理念就是需将单核CPU的性能发挥到极致,这也便是单机Redis实例QPS性能只能在10W左右的根因,因其无法像多线程程序那般通过堆加CPU核实现高并发。然而,对于普通的场景来说,这个单机性能也是足够用的。
IO多路复用的事件机制:
上面过说,Redis单线程需要用来处理网络IO和Key-value数据信息; (Redis6.0版本网络IO改为多线程)
但是传统的网络编程模型是阻塞式的。如果按照这种阻塞模型的设计,那么Redis的主线程接受到连接请求并等待数据输入时,主线程是被阻塞的,不能够处理KV数据信息。解决这个问题,可以引入多线程,专门启动一个新线程负责处理网络IO,主线程只进行KV数据处理(Redis6.0设计方案),6.0之前则是IO多路复用的机制,即一个线程负责处理网络IO和KV数据;
代码语言:javascript复制accept(listenfd) #接收到请求,等待数据输入
IO多路复用解释:
为每个客户端创建一个线程,服务器端的线程资源很容易被耗光。
当然还有个聪明的办法,我们可以当每接收一个客户端连接后 (只接收连接请求,还没接收请求的数据信息),将这个文件描述符(connfd)放到一个数组里。
代码语言:javascript复制fdlist.add(connfd);
然后弄一个新的线程去不断遍历这个数组,调用每一个元素的非阻塞 read 方法 (read不会阻塞线程,accept会阻塞线程)
代码语言:javascript复制while(1) {
for(fd <-- fdlist) {
if(read(fd) != -1) {
doSomeThing();
}
}
}
这样,我们就成功用一个线程处理了多个客户端连接。
你是不是觉得这有些多路复用的意思?
并且我们将遍历的委托给操作系统内核,它提供给我们一个有这样效果的函数 。我们将一批文件描述符通过一次系统调用传给内核,由内核层去遍历,真正高效解决这个问题。
Redis中IO多路复用的事件机制解释:
Redis
中将 IO多路复用 与 事件机制进行整合使用。在Linux操作系统中是epoll (IO多路复用的一种) 事件机制;
下图是基于epoll机制的Redis IO
模型。图中的多个FD是连接套接字。通过epoll机制,让内核监听这些套接字。此时,Redis
线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis
可以同时和多个客户端连接并处理请求,从而提升并发性。
为了在请求到达时能通知到Redis
线程,epoll提供了基于事件的回调机制,即针对不同事件的发生,会调用不同的处理函数。
那么,回调机制是怎么工作的呢?epoll一旦监测到FD上有数据到达时,就会触发相应的事件。这些触发的事件会被放进一个事件队列并且通知Redis
线程对该事件队列不断进行处理。这样一来,Redis无需一直轮询是否有请求实际发生,这就可以避免造成CPU资源浪费。 同时,Redis
在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。
解释:Redis
接收到连接请求,把连接请求交给操作系统内核,内核接收到该请求数据后返回事件到队列中并通知主线程。主线程遍历事件队列,将对应的数据从内核缓冲区拷贝到用户缓冲区,然后在用户空间对请求数据(get/set)进行处理。
参考资料:
- 你管这破玩意叫 IO 多路复用? (qq.com)
- 高性能IO模型:为什么单线程Redis能那么快? - 掘金 (juejin.cn)