通俗分析单线程的Redis为什么这么快
1、纯内存操作
Redis 绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,查找和操作的时间复杂度都是O(1)。
拿笔记本电脑来说,假设主频是3.1G,也就是说每秒可以执行3.1*10^9个指令。所以说CPU看世界是非常非常慢的,内存比它慢百倍,磁盘比他慢百万倍。
2、单线程+多路IO复用机制
想了解下什么是多路复用以及多路复用常见的实现方式?请点击这里
单线程想要实现高性能,必须和多路IO复用机制结合在一起。因为单线程最怕的是什么?最怕的就是阻塞,以为所有的程序都是串行的,一个点堵住了,后面所有的程序执行都会成为问题。
举个形象的例子说下,就像是汽车行驶的单行道,中间有一辆车抛锚了,后面的车全都堵住了,根本过不去。
现在很多文章在分析为什么Redis单线程更快的时候,都要顺便踩一脚多线程的缺点,比如说上下分切换成本比较高。
但是我现在却感觉不能把多线程有如此的缺点才导致了Redis单线程的快,而是Redis单线程+多路IO复用这种黄金组合的存在已经使得Redis抗住大并发,实现了高性能,就没有必要再去用更复杂的多线程来实现。
用redis官方的回答就是:redis这种单线程+多路IO复用的方式实现的高性能,如果再想提高性能,CPU已经不是瓶颈。没有必要利用多线程榨干CPU来实现高性能。
所以,我们再分析Redis高性能的时候,可以专注于分析单线程+多路IO的强大作用。
客户端与 redis 的一次通信过程:
- 客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。
- 文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
- 假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
- 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
这样便完成了一次通信。
备注:
- AE_READABLE 事件 从字面就比较容易理解,这个直接告诉IO多路复用程序,我可以读了,赶紧来处理我吧
- AE_WRITABLE 事件 从字面就比较容易理解,这个直接告诉IO多路服用程序,我可以写了,赶紧来处理我吧
子模块的选择:
众所周知,redis是可以安装在多个平台的,比如win,Mac,Linux等等。但是我们也已经知道,多路IO复用的实现在不同的系统下也是不一样的。那么redis怎么保证在不同的系统下都使用系统的多路复用功能?下面大概讲解下。
redis为不同的系统都的多路IO复用功能的实现都进行了接口的封装,然后最顶层给出了一个统一的接口。也就意味着,无论你在什么系统下,redis都可以自动切换接口来实现多路复用功能。下面我们通过源码说明下:
因为 select 函数是作为 POSIX 标准中的系统调用,在不同版本的操作系统上都会实现,所以将其作为保底方案。
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
参考资料:
https://www.jianshu.com/p/68e949a5ef79