Redis之I/O多路复用技术(multiplexing)

在面试中通常会有这样子的场景
↓↓↓↓↓↓
面试官:看你的简历写到项目中有用到redis,可以聊聊redis吗
求职者:可以哇。我在项目中主要使用了redis做商品信息的缓存,我会先从缓存中拿商品信息,如果缓存失效了再去数据库拿商品信息,最后更新缓存,这样子做直接提高了程序的性能并减少了DB的压力。
求职者:redis很快,主要是因为完全基于内存,而且是单线程,使用了I/O多路复用模型。
面试官:那你了解I/O多路复用技术在redis中的应用吗
求职者:不是很了解。。。

在了解I/O多路复用之前,我们可以先了解一下传统的I/O模型

  • 同步阻塞I/O模型

首先,要从你常用的IO操作谈起,比如read和write,通常IO操作都是阻塞I/O的,也就是说当你调用read时,如果没有数据收到,那么线程或者进程就会被挂起,直到收到数据。无法处理并发

  • 同步非阻塞IO模型

当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,这样是不会阻塞线程了。此时用户进程需要不断轮询,如果轮询频繁,则浪费了大量的CPU资源;如果轮询频率低,则不能实时地获取数据。过度浪费CPU资源

  • 异步IO模型

当调用read时,不需要等到数据返回就可以继续去干别的事情,调用write的时候同理。但是频繁的切换线程会出现浪费过多CPU资源的问题。

多路复用I/O模型

这里"多路"指的是多个网络连接,"复用"指的是复用同一个线程。

多路复用I/O模型是一种同步I/O模型。实现一个线程监听多个文件句柄(也叫做文件描述符,FileDescription,简称FD),当有一个FD就绪时,则通知对应的应用程序进行读写操作。当没有FD就绪时,就会阻塞并交出CPU。

多路复用I/O模型是利用select、epoll、evport和kequeue可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作(比如多线程之间的切换,频换的切换线程会造成大量的资源浪费)。

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。
Redis之I/O多路复用技术(multiplexing)_第1张图片
大概对这张图做一个解释:

  • 当进程调用select,进程就会被阻塞
  • 此时内核会监视所有select负责的的socket,当socket的数据准备好后,就立即返回。
  • 进程再调用read操作,数据就会从内核拷贝到进程。
Redis为什么要引入多路复用I/O技术

I/O多路复用的本质是同步阻塞I/O模型,但是,它最大的优势在于可以在一次阻塞中监听多个文件描述符(FD)。我们带入redis的场景,来思考一下redis为什么使用多路复用I/O技术

  • 首先采用普通的同步阻塞I/O,那么Redis可能会在一个客户端上长期阻塞。该客户端可能长期没有数据到达,而Redis需要处理多个客户端的通信,当其他客户端有请求到达时,Redis则无法处理了,这显然是无法接受的。
  • 如果使用同步非阻塞I/O,那么就需要不断轮循客户端,那么这种频繁的轮循会很浪费CPU资源,如果轮循不频繁,那么可能就会出现数据不能实时获取的问题。
  • 如果使用 异步IO模型,线程的创建和频繁的上下文切换会浪费更多的资源。其次Redis本身就是单进程单线程的模式工作,多线程等待多个客户端显然与其系统思想不符。

综上,多路复用I/O技术是首选。

IO多路复用的三种实现方式
select poll epoll
数据结构 bitmap 数组 红黑树
最大连接数 1024
FD拷贝 每次调用select拷贝 每次调用poll拷贝 fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
工作效率 轮询:O(n) 轮询:O(n) 回调:O(1)
  • select函数
    • 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024
    • 每次调用select,都需要把FD集合从用户态拷贝到内核态,这个开销在FD很多时会很大
    • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)
  • poll函数
    • poll与select相比,只是没有FD的限制,其它基本一样
  • epoll函数
    • epoll只能在linux下工作
    • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是"高速"模式
      • LT模式下,只要这个FD还有数据可读,每次epoll_wait都会返回它的事件,提醒用户程序去操作
      • ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论FD中是否还有数据可读。所以在ET模式下,read一个FD的时候一定要把它的buffer读完,或者遇到EAGAIN错误
事件处理器

监听到有读、写事件发生以后,接着要进行事件处理。Redis会为每个监听的文件描述符绑定一个读事件处理器或者写事件处理器,当事件发生时,会执行相应的事件处理函数。对于不同的客户端绑定的事件处理器可能会不相同。

  • 文件事件(file event):Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作
  • 时间事件(time event):Redis服务器的一些操作(比如serverCron函数)需要在给定的时间执行,而时间事件就是服务器对这类定时操作的抽象
文件事件处理器
  • 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的时间处理器来处理这些事件
时间事件处理器

时间事件分为以下两类:

  • 定时事件:让一段程序在指定的时间之后执行一次,比如,让程序A在当前时间30秒后执行一次
  • 周期性事件:让一段程序每隔指定时间就执行一次,比如,让程序B每隔30秒就执行一次

时间事件主要有以下三个属性构成:

  • id:服务器为时间事件创建的全局唯一ID(标识),ID按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大
  • when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间
  • timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件
总结
  • Redis使用的是同步阻塞I/O模型,I/O多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
  • Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
  • Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
参考

http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html

你可能感兴趣的:(Redis)