不知道你有没有被问到过这样一个问题:Redis为什么这么快?
- 基于内存
- 数据结构
- epoll和IO多路复用
- ......
多路复用需要解决的问题
在使用I/O多路复用之前,最简单典型的处理并发多客户端连接的方案就是同步阻塞网络I○模型
简单来讲就是用一个进程来处理一个网络连接/用户请求(来一个new一个)
这种方式的优点就是非常容易理解,代码逻辑也非常符合人的直线型思维,但是它的性能很差,即每个网络连接/用户请求来都得占用一个进程来处理,我们举个🌰,就像是每新来一个学生就新聘用一个老师给他,医院每来一位患者就配一个医生,这是非常耗资源甚至不可能的,我们回到计算机上,一台服务器上创建不了多少个进程的(进程在Linux上是一个很笨重的东西,光是上下文切换一次就得几个微秒)。
所以为了高效地对海量用户提供服务,需要让一个进程能同时处理很多个tcp连接,没错~I/O多路复用。
假设现在有一个进程保持了10000条连接,那么我们如何保证高效地对海量用户提供服务?发现哪条连接上有数据可读?哪条连接可写?
遍历,没错,我的第一反应就是遍历,但是再仔细想想有10000条连接,虽然用循环遍历的方式来发现IO事件确实可行,但是不是效率太低了?有没有一种更高效的方法?可以在很多连接中的某条上有IO事件发生的时候直接快速把它找出来?
当然有( ̄∇ ̄)/ 并且Linux操作系统已经都做好了,它就是IO多路复用机制(这里的复用指的就是对进程的复用),生动些理解就是"谁好了谁举手🙋"。
I/O多路复用模型
简介
先解释下这个"I/O多路复用模型",I/O指的是"网络I/O","多路"指多个客户端连接(连接就是套接字描述符,socket或者channel),指的是多条TCP连接,"复用"指用一个进程来处理多条的连接。一句话概括下就是使用单进程就能够实现同时处理多个客户端的连接。IO多路复用类似一个规范和接口,它的落地实现可以分为select、poll、epoll三个阶段,接下来我们来分析下这个epoll函数是如何实现这些操作的。
Redis单线程如何处理那么多并发客户端连接
Redis的IO多路复用
Redis利用epoll来实现IO多路复用,将连接信总和事件放到队列中,一次放到文件事件分派器,事件分派器将事件分发给事件处理器。
Redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以I/O操作在一般情况下往往不能直接返回,这会导致某一文件的/O阻塞导致整个进程无法对其它客户提供服务,而/O多路复用就是为了解决这个问题而出现所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要select、pol川、epol川来配合。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
Redis服务采用Reactor的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:
- 多个套接字
- IO多路复用程序
- 文件事件分派器
- 事件处理器
因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
参考《Redis设计与实现》
Redis开发了自己的网络事件处理器(基于Reactor模式)------文件事件处理器( File Event Handler)。文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个Socket,并根据Socket目前执行的任务来为其关联不同的Handler。当被监听的Socket准备好执行连接(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的File Event 就会产生,这时Handler就会调用Socket之前关联好的事件处理器来处理这些事件。
然文件事件处理器以单线程方式运行,但通过使用/O多路复用程序来监听多个Socket,该Handler就实现了高性能的网络通信模型,又可以继续使用单线程与Redis服务器中同样使用单线程运行的其他模块进行对接。
从Redis 6开始通过多个IO线程解决网络IO问题(多线程优势,高效),但真正执行命令时,仍使用单线程操作(简单+无锁+安全+高性能),一举两得。
举个🌰
同步与异步、阻塞与非阻塞这4个概念可以两两组合,就形成了我们常听到的同步阻塞、同步非阻塞、异步阻塞、异步非阻塞,我们举个点奶茶🥤的例子来帮助大家理解( ̄∇ ̄)/~
张三想喝奶茶,于是他走到楼下的奶茶店,跟收银台负责点餐的服务员说,"我要一杯不额外加糖的正山小种~"
然后可能出现如下4种情况:
- 服务员跟张三说:"你的奶茶马上就好!先别离开哦~",于是张三就站在等候区,什么也没心情干,就等着服务员叫号。
- 服务员跟张三说:"你的奶茶马上就好!先别离开哦~",于是张三就站在等候区,一边刷B站视频一边等着服务员叫号。
- 服务员跟张三说:"你的奶茶还要等一会才能做好,你可以先去别的地方逛一逛~",张三则站在收银台前寸步不离,什么也没心情干,就等着服务员叫号。
- 服务员跟张三说:"你的奶茶还要等一会才能做好,你可以先去别的地方逛一逛~",张三于是拿着小票去隔壁的书店逛了起来,等着服务员叫号。
大家能分辨出哪个是同步阻塞?哪个是异步非阻塞嘛?
自问自答(O(∩_∩)O哈哈~
1:同步阻塞、2:同步非阻塞、3:异步阻塞、4:异步非阻塞
正儿八经的~
- 同步:调用者要一直等待结果的通知才能进行后续的操作,现在就要,我可以等,直到结果出来为止
- 异步:指调用方先返回,然后计算调用结果,计算完了以后再通知并返回给调用方(异步调用要获得结果一般通过回调)
同步和异步讨论的对象是服务提供者 (即被调用者),重点在于获得调用结果的消息通知方式上。
- 阻塞:调用方一直在等待而且别的事情什么都不做(当前进程(线程)会被挂起阻塞)
- 非阻塞:调用发出去后,调用方会先做别的事情(当前进程(线程)会立刻返回,不会被挂起阻塞)
与同步和异步不同,这两个概念讨论的是服务请求者(即调用方),重点在于服务请求者等消息时候的行为(是什么都不干😳还是可以去干别的事)
好啦,初步了解了什么是IO多路复用,下一篇我们继续深入介绍讲到IO多路复用就不得不提的Unix网络编程中的5种IO模型^ ^