原因
- 纯内存操作,而内存的响应事件大概在100纳秒左右,这是redis之所以快的一个基础条件
- 基于reactor单线程模型,减少了线程切换
- 基于IO多路复用的模式,提高了事件处理效率
咱们先了解一下IO多路复用
“多路”指的是多个网络连接,“复用”指的是复用同一个线程。 服务器采用单线程利用 select、poll、epoll等系统调用机制,同时监察多个流的 I/O 事件的一种模型,使其能支撑更多的并发连接请求。在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,然后程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且依次顺序地处理就绪的流,让单个线程的利用率达到最高,同时减少网络 IO 的时间消耗。
常见的I/O多路复用处理类型是 select、poll、epoll、kqueue、/dev/poll、eventport等(前三者最为常见),而**redis的io模型主要是基于epoll实现的**,不过它也提供了 select和kqueue的实现,默认采用epoll,下面对前三种多路复用的实现类型逐一介绍。
在介绍三种主要的实现之前,先说下fd的概念
fd是一个文件描述符,意思是表示当前文件处于可读、可写还是异常状态。
实现1:select(bit实现)
简单来说,就是当有I/O事件发生时,select操作并不知道是哪几个流发生的事件,因此会对所有流进行无差别地轮询。
缺点:
- 单个进程打开的fd是有限制的,默认1024(可通过FD_SETSIZE设置)
- I/O开销大:每次调用select都需要把fd从用户态拷贝到内核态
- 浪费CPU:对socket扫描是线性轮询,效率较低
实现2:poll(链表实现)
与select基本一致,只是因为其采用链表结构,因此并没有最大连接数的限制。(联想到阿里巴巴Java开发手册里边提到的,不能用默认Executors工具类创建线程池,如newCachedThreadPool和newFixedThreadPool,原因:
1、newCachedThreadPool用的SynchronousQueue,也就是只做转发不存储元素的队列,而且默认最大线程数为int最大值,容易因任务过多导致OOM
2、newFixedThreadPool用Queue是调LinkedBlockingQueue无参构造器创建的,默认大小为int最大值,当任务量庞大时,容易导致内存溢出。)
缺点:
- 与select基本一致(轮询效率低、I/O开销大)
实现3:epoll(event-poll :事件驱动)
对比select与poll,epoll不再对所有流进行无差别的询问,而是反过来,把哪个流发生了什么事件直接通知我们(实际实现是将需要处理的事件挂载到一颗红黑树中),使处理事件的效率大大提高,基本是O(1)的时间复杂度。
两种模式:
1、LT - 默认的模式。LT是只要fd上还有数据可读就会一直返回该fd的事件,提醒用户进行处理。
2、ET - “高速” 模式。ET与LT相反,只返回一次,直到再有数据流入后才会再次提醒。(因此读取一个fd必须把其buffer读完,否则可能数据丢失)
优点:
- 不像select一样,epoll没有最大并发连接限制
- 不再需要轮询所有流,只处理发生事件的流,效率得到极大的提升
- 使用mmap文件映射内存的方式(内存拷贝)加速了与内核空间的传递,减少了赋值的开销。
缺点:
- 仅支持Linux系统
举个栗子来对比这三者
比如你去一家餐厅用餐,坐在餐椅上等待服务员过来帮忙下单,如果老板采取select、poll的机制,相当于服务员对每个餐桌都是一视同仁的,不会去区分哪桌是已经在吃上了,哪桌是吃完了,哪桌正在焦急地等待点菜,都挨个问一遍有没有需要点菜的,最坏情况下O(n)(n-为桌子数)才问到你这,此时如果餐厅桌子很多,可能你已经饿坏走人了。但如果老板采用epoll就不同了,每个桌子安装一个按铃,需要点菜/服务直接按按铃即可,服务员看到屏幕上显示的按了按铃的桌子号,就能直接定位到你的桌子,那么效率将大大提高!
参考:
网友评论