在看到一篇《单线程Redis为什么这么快?》的文章时,作者说出了如下三点原因:(1)纯内存操作、(2)单线程操作,避免了频繁的上下文切换、(3)采用了非阻塞I/O多路复用机制。前两点很好理解,本篇文章说说我对第三点的理解

Java NIO 好像也用到这个技术(还没学习过NIO,先挖个坑)

从老版本tomcat说起

在老的tomcat(tomcat 6以前)中,使用基于多线程的体系结构,通常会使用多线程来处理客户端的请求,每当接收到一个请求,便开启一个独立的线程来处理。

这种方式虽然是直观的,但是仅适用于并发访问量不大的场景,因为线程需要占用一定的内存资源,且操作系统在线程之间的切换也需要一定的开销,当线程数过多时显然会降低web服务器的性能。一个典型的设计如下:

这里写图片描述

对于上图的这种结构,还有一点需要注意,就是线程的粒度太大。每一个线程把一次交互的事情全部做了,包括read、decode、compute等等。

我们采用线程池的方式,线程的数目是有上限的,假设有10个,那么现在来了一个连接,分配了一个线程,此时执行完了读取,正在执行写入返回,那么这个读取的过程其实可以拿出来让后续的连接再用,当前的请求已经是不会再用了,但是仍然占据了这个资源,这是不合理的。

所以说,线程同步的粒度太大了,限制了吞吐量。应该把一次连接的操作分为更细的粒度或者过程,这些更细的粒度是更小的线程。整个线程池的数目会翻倍,但是线程更简单,任务更加单一。

非阻塞I/O多路复用

上面说的线程粒度细化,其实就是多路复用出现的原因,在多路复用中,这些被拆分的小线程或者子过程对应的是handler,每一种handler会出处理一种event。

这里会有一个全局的管理者selector,把需要做的事注册到channel,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么selector线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。典型的注册事件有连接,读取和写入,当然我们就需要为这些事件分别提供处理器,每一个处理器可以采用线程的方式实现。一个连接来了,显示被读取线程或者handler处理了,然后再执行写入,那么之前的读取就可以被后面的请求复用,吞吐量就提高了。
这里写图片描述

总结

Redis使用了多路复用,也就是说Redis只有一个线程(select)来处理请求,但是对于具体请求的处理是不是采取多线程的方式无法确定。

有人这样说:redis并不是单线程,至少他在做持久化的时候并不是单线程

补充

2018-06-28补充:

所谓的阻塞与非阻塞是相对于某一个对象来说的,就拿上面的内容来说,是指事件处理函数非阻塞。下面举一个从知乎看到的例子:

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。

同步就是烧开水,要自己来看开没开;异步就是水开了,然后水壶响了通知你水开了。阻塞是烧开水的过程中,你不能干其他事情(即你被阻塞住了);非阻塞是烧开水的过程里可以干其他事情。同步与异步说的是你获得水开了的方式不同。阻塞与非阻塞说的是你得到结果之前能不能干其他事情。两组概念描述的是不同的内容。

同步和异步关注的是消息通信机制 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

我现在又有了新的疑问,IO多路复用与阻塞非阻塞的关系?与同步异步的关系?