https://draveness.me/redis-io-multiplexing
redis是一个单线程的程序,在IO方面由于是单线程,所以为了提高IO的效率,使用了多路复用的IO模型。
Blocking I/O
最原始的Blocking IO为阻塞的IO。读写操作等待用户输入或输出都是阻塞的,每次的IO都是要等待的
当使用 read 或者 write 对某一个文件描述符(File Descriptor 以下简称 FD),如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。
模型图如下(这种图的风格看着好舒服啊):
这种模型在每次有一个FD来的时候会阻塞住,这时候FD的数据在系统中还没有准备好,当准备好了的时候在从内核准备好的FD中把数据复制到进程的内存里,这时候阻塞才会释放。 每次凡是有一个FD那么就会阻塞住其他的。其实一次IO操作可以分成两个操作部分,一个是等待数据准备好,然后在从内核复制,这第二步其实是真正的IO复制操作。有时可能由于一次网络连接,连接准备好了,但是由于网络延迟没有数据传输过来,导致这个整个过程特别的耗时。那么我们怎么改进呢?
其实操作系统已经考虑了这种情况,每个操作系统都为我们提供了可以检测有哪些FD已经准备好了,所以我们不用在阻塞整个IO操作了,我们可以一次连接多个FD,然后遍历这些FD,有哪些已经准备好了,我们只需要使用这些准备好的进行IO操作即可。
图片如下:
具体有操作系统有哪些函数可以供我们使用呢?
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 能以单进程运行的同时服务成千上万个文件描述符,避免了由于多进程应用的引入导致代码实现复杂度的提升,减少了出错的可能性。