最近在看 UNIX 网络编程并研究了一下 Redis 的实现,感受 Redis 的源代码十分适合阅读和分析,其中 I/O 多路复用(mutiplexing)部分的实现很是干净和优雅,在这里想对这部分的内容进行简单的整理。react
为何 Redis 中要使用 I/O 多路复用这种技术呢?web
首先,Redis 是跑在单线程中的,全部的操做都是按照顺序线性执行的, 可是因为读写操做等待用户输入或输出都是阻塞的,因此 I/O 操做在通常状况下每每不能直接返回,这会致使某一文件的 I/O 阻塞致使整个进程没法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。redis
先来看一下传统的阻塞 I/O 模型究竟是如何工做的:当使用 read 或者 write 对某一个文件描述符(File Descriptor 如下简称 FD)进行读写时,若是当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操做做出响应,致使整个服务不可用。数据库
这也就是传统意义上的,也就是咱们在编程中使用最多的阻塞模型:
编程
阻塞模型虽然开发中很是常见也很是易于理解,可是因为它会影响其余 FD 对应的服务,因此在须要处理多个客户端任务的时候,每每都不会使用阻塞模型。设计模式
虽然还有不少其它的 I/O 模型,可是在这里都不会具体介绍。服务器
阻塞式的 I/O 模型并不能知足这里的需求,咱们须要一种效率更高的 I/O 模型来支撑 Redis 的多个客户(redis-cli),这里涉及的就是 I/O 多路复用模型了:网络
在 I/O 多路复用模型中,最重要的函数调用就是 select,该方法的可以同时监控多个文件描述符的可读可写状况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。并发
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每个网络链接其实都对应一个文件描述符)
框架
文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。
虽然整个文件事件处理器是在单线程上运行的,可是经过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提升了网络通讯模型的性能,同时也能够保证整个 Redis 服务实现的简单。
I/O 多路复用模块封装了底层的 select、epoll、avport 以及 kqueue 这些 I/O 多路复用函数,为上层提供了相同的接口。
简要了解该模块的功能,整个 I/O 多路复用模块抹平了不一样平台上 I/O 多路复用函数的差别性,提供了相同的接口.
由于 Redis 须要在多个平台上运行,同时为了最大化执行的效率与性能,因此会根据编译平台的不一样选择不一样的 I/O 多路复用函数做为子模块,提供给上层统一的接口;在 Redis 中,咱们经过宏定义的使用,合理的选择不一样的子模块
由于 select 函数是做为 POSIX 标准中的系统调用,在不一样版本的操做系统上都会实现,因此将其做为保底方案:
Redis 会优先选择时间复杂度为 O(1) 的 I/O 多路复用函数做为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的这些函数都使用了内核内部的结构,而且可以服务几十万的文件描述符。
可是若是当前编译环境没有上述函数,就会选择 select 做为备选方案,因为其在使用时会扫描所有监听的描述符,因此其时间复杂度较差 O(n),而且只能同时服务 1024 个文件描述符,因此通常并不会以 select 做为第一方案使用。
Redis 对于 I/O 多路复用模块的设计很是简洁,经过宏保证了 I/O 多路复用模块在不一样平台上都有着优异的性能,将不一样的 I/O 多路复用函数封装成相同的 API 提供给上层使用。
整个模块使 Redis 能以单进程运行的同时服务成千上万个文件描述符,避免了因为多进程应用的引入致使代码实现复杂度的提高,减小了出错的可能性。
Redis为何是单线程 ?
由于CPU不是Redis的瓶颈。Redis的瓶颈最有多是机器内存或者网络带宽。(以上主要来自官方FAQ)既然单线程容易实现,并且CPU不会成为瓶颈,那就瓜熟蒂落地采用单线程的方案了。关于redis的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求,参见:How fast is Redis?(https://link.zhihu.com/?target=https%3A//redis.io/topics/benchmarks)
若是万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其余核闲置,那怎么办?
那也很简单,你多起几个Redis进程就行了。Redis是keyvalue数据库,又不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪一个Redis进程上就能够了。redis-cluster能够帮你作的更好。
单线程模型
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,因为Redis是单线程来处理命令的,全部每一条到达服务端的命令不会马上执行,全部的命令都会进入一个队列中,而后逐个被执行。而且多个客户端发送的命令的执行顺序是不肯定的。可是能够肯定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
单线程模型每秒万级别处理能力的缘由
(1)纯内存访问。 数据存放在内存中,内存的响应时间大约是 100纳秒 ,这是Redis每秒万亿级别访问的重要基础。
(2)非阻塞I/O ,Redis采用epoll作为I/O多路复用技术的实现 ,再加上Redis自身的事件处理模型将epoll中的链接,读写,关闭都转换为了时间,不在I/O上浪费过多的时间。
(3)单线程 避免了线程切换和竞态产生的消耗 。
(4)Redis采用单线程模型,每条命令执行若是占用大量时间, 会形成其余线程阻塞,对于Redis这种高性能服务是致命的,因此Redis是面向高速执行的数据库。
内部实现采用epoll,采用了epoll+本身实现的简单的事件框架。 epoll中的读、写、关闭、链接都转化成了事件,而后利用epoll的多路复用特性, 毫不在io上浪费一点时间
这3个条件不是相互独立的,特别是第一条,若是请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说redis为特殊的场景选择了合适的技术方案。
参考:
https://draveness.me/redis-io-multiplexing
http://www.javashuo.com/article/p-vcsnfnsj-cv.html