Redis的IO多路复用原理

Posted 阿斌Java之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis的IO多路复用原理相关的知识,希望对你有一定的参考价值。

什么是阻塞,非阻塞,异步同步,select,poll,epoll?今天我们用一遍文章解开这多年的迷惑。

首先我们想要通过网络接收消息,是这样的一个步骤。

  1. 用户空间向内核空间请求网络数据
  2. 内核空间把网卡数据读取到内核缓冲区
  3. 将内核缓冲区的数据复制到用户缓冲区

根据我们请求数据的情况不同,以及内核缓冲区到用户缓冲区的不同,分为了阻塞,非阻塞,异步同步的区别。

在《UNIX网络编程》一书中,总结归纳了5种I0模型:

  • 阻塞 I0 ( Blocking I0)
  • 非阻塞 I0 (Nonblocking I0)
  • I0多路复用(I0 Multiplexing)
  • 信号驱动I0 (Signal Driven I0 )
  • 异步I0 (Asynchronous I0)

阻塞IO

  1. 用户应用请求内核是否有新的网络数据
  2. 如果没有数据,就阻塞直到有数据到来
  3. 等待内核将数据拷贝到用户空间
  4. 用户应用处理数据

以上可以看出来,根据等待数据的方式不同,分为阻塞和非阻塞。

阻塞IO在请求内核数据的时候,没有数据就会一直阻塞直到获取数据。

非阻塞IO

  1. 用户应用请求内核是否有新的网络数据
  2. 如果没有数据,内核直接返回没数据,用户应用可以隔一段时间再来请求。
  3. 等待内核将数据拷贝到用户空间
  4. 用户应用处理数据

非阻塞IO在等待内核数据的时候,没有数据就会得到没数据的结果,应用可以进行其他动作。

同步IO

同步IO的主要看内核数据到用户空间的过程是同步进行的就是同步IO

异步IO

异步IO首先是非阻塞IO,区别在于成功标志的时机。异步IO连内核到用户态的数据拷贝都是异步的,直到数据拷贝完成,才会回调一个信号,通知一切已经准备完成。用户应用此时就可以直接处理结果了。

总结

阻塞非阻塞指的是在获取结果上是否会阻塞等待结果完成

同步异步指的是是否会参与IO读写,或者是等待读写成功的回调

redis的IO多路复用

如果是阻塞IO也就是BIO,那么在一个fd(文件描述符)没有数据的时候,就是阻塞一直等待,如果同时有多个fd,对于单线程来说,只能一直等第一个有数据,然后再接着处理第二个,效率很慢。

就像顾客点餐,要一直等到第一个人点完餐,后面的人才有机会。BIO也有个解决办法,一般是增加多线程,每个线程都维护一个fd,就相当于为每个顾客都添加一个点餐台。在fd足够多的情况下,会有大量的线程被创建,线程可是有上限的,开销也大(更多线程需要更多的内存空间)。

如果是非阻塞IO也就是NIO,会有顾客没点完餐,然后造成CPU一直在询问一直空转的情况。

因此引入了IO多路复用模型:利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源

文件描述符( File Descriptor) :简称FD,是一个从0开始递增的无符号整数,用来关联Linux中的一一个文件。在Linux

中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket),

这时候每来一个顾客(FD),我们就会给他一个开关(注册进监听事件),一个服务员(一个线程)等待开关亮起(阻塞等待事件)。有顾客完成,就会按下开关,一定的频率下开关会亮起(事件通知),服务员会选取按下开关的一批人,给他们点餐(批量处理事件)。

IO多路复用的实现有select,poll,epoll,我们来看看他们的优缺点。

select

select是Linux中最早的I/O多路复用实现方案,并且windows操作系统上只支持select。这就是为啥window发挥不出redis的最大性能的一个原因。

select函数执行流程

  1. 用户空间创建fd_set,把需要监听的位置置1,比如 1,2,5
  2. 用户空间拷贝fd_set(注册的事件集合)到内核空间
  3. 内核遍历所有fd文件,并将当前进程挂到每个fd的等待队列中,当某个fd文件设备收到消息后,会唤醒设备等待队列上睡眠的进程,那么当前进程就会被唤醒
  4. 内核如果遍历完所有的fd没有I/O事件,则当前进程进入睡眠,当有某个fd文件有I/O事件或当前进程睡眠超时后,当前进程重新唤醒再次遍历所有fd文件
  5. 内核有事件产生,会把fd_set中有事件的位置保留为1,没有事件的位置擦除为0.
  6. 内核拷贝fd_set给用户空间
  7. 用户空间线程被唤醒,遍历fd_set为1的位置,确认是哪些fd有就绪事件,然后开始处理
  8. 用户空间处理完事件,再一次将要监听的fd_set设置为1,重复之前的监听动作

根据上面可以很清楚的看出整个执行流程在用户空间和内核空间的切换。

select函数的缺点

  • 单个进程所打开的FD是有限制的,通过 FD_SETSIZE 设置,默认1024
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
  • 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除
  • select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
  • 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的

poll运行流程

①创建pollfd数组, 向其中添加关注的fd信息,数组大小自定义

②调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限

③内核遍历fd,判断是否就绪

④数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n

⑤用户进程判断n是否大于0

⑥大于0则遍历pollfd数组,找到就绪的fd

与select对比

  • select模式中的fd_ set大小固定为1024,而pollfd在内核中采用链表,理论上无上限.
  • 监听FD越多,每次遍历消耗时间也越久,性能反而会下降

poll还是没有解决需要遍历判断fd事件的方式,只是增加了监听数量,在fd很多的情况下,性能下降的更加严重

epoll

epoll可以理解为event pool,不同与select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

他主要有三个函数,epoll的执行流程

  1. 调用epoll_create创建一个eventpoll结构体,这个结构体有一个监听事件红黑色,和一个就绪链表(这个链表只会存放就绪fd,避免我们无效的遍历所有fd)

  1. 调用epoll_ctl向eventpoll中注册一个监听的fd,并且注册上fd对应事件的回调函数。

  1. 调用epoll_wait开始阻塞等待事件到来
  2. 内核将监听到的事件添加一份到就绪队列list_head

  1. 内核唤醒用户线程,并将就绪链表拷贝到用户空间

  1. 用户应用只需要关心这些就绪的fd事件,直接取出结构体里关联的回调函数进行回调即可处理事件。

对应的redis的server执行流程

  1. 调用epoll_create创建一个eventpoll结构体
  2. 调用epoll_ctl向eventpoll中注册一个监听连接的serverSocket,并关联上处理accept事件的函数
  3. 调用epoll_wait阻塞等待fd事件(等待客户端连接)
  4. 用户程序被唤醒,事件到来(现在只有连接事件)。根据生成的客户端的FD,调用epoll_ctl注册一个监听,并且关联上处理read事件的函数和处理write事件的函数。
  5. 继续调用epoll_wait阻塞等待fd事件(等待客户端连接或客户端命令执行请求)
  6. 用户程序被唤醒,事件到来(连接事件或者命令执行请求),假设是客户端执行请求事件,根据客户端的fd对应的read事件直接调用绑定的回调函数来处理,将结果再写回到fd缓存中。
  7. 继续调用epoll_wait等待accept,read,write事件。

epoll优点

  • EPOLL支持的最大文件描述符上限是整个系统最大可打开的文件数目, 1G内存理论上最大创建10万个文件描述符
  • 每个文件描述符上都有一个callback函数,当socket有事件发生时会回调这个函数将该fd的引用添加到就绪列表中,select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可
  • select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多

读事件很好理解,有一个读事件就立马处理请求,怎么理解写事件?

当socket 写缓冲区已满,假如设置了非阻塞I/O,应用程序调用send会返回EAGAIN,告诉应用程序写缓冲区已满,下次再来尝试调用,这时候就有一个尝试的时机问题,应用程序怎么知道socket 缓冲区可写呢?如果频繁调用send,会浪费CPU。这时候,epoll就排上用场了,对socket 设置写事件,并添加到 epoll中,应用程序调用epoll_wait,当该socket 的写缓冲有空余时,就返回对应的写事件,应用程序这时候就可以调用send,发送数据。

所以写事件是用来告诉程序,写缓冲是空余的。一般情况下fd都是有写事件的。但是在写缓冲区满了的时候,就会频繁触发写事件。所以我们可以一开始不监听写事件,直到发现数据量可能大于缓冲区,再监听写事件

参考:高效处理写事件

参考

select poll epoll

黑马多路复用视频

Redis单线程还是多线程?IO多路复用原理


目录

专栏导读

🏆作者简介:哪吒,CSDN2022博客之星Top1、CSDN2021博客之星Top2、多届新星计划导师✌、博客专家💪 ,专注Java硬核干货分享,立志做到Java赛道全网Top N。

🏆本文收录于Java基础教程系列(进阶篇),本专栏是针对大学生、初级Java工程师精心打造,针对Java生态,逐个击破,不断学习,打通Java技术栈

🏆订阅后,可以阅读Java基础教程系列(进阶篇)中全部文章包含Java基础、Java高并发、Spring、MySQL等Java进阶技术栈

🏆还可以订阅其姐妹篇Java基础教程系列,包含全部Java基础知识点、Java8新特性、Java集合、Java多线程、Java代码实例理论结合实战,实现Java的轻松学习

🏆哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师

🏆面试福音:10万字208道Java经典面试题总结(附答案)

大家好,我是哪吒。

上一篇分享了图解Redis,Redis主从复制与Redis哨兵机制,今天分享一下Redis为什么选择单线程?Redis为什么这么快?,实现快速入门,丰富个人简历,提高面试level,给自己增加一点谈资,秒变面试小达人,BAT不是梦。

一、Redis版本迭代

  1. Redis2.6,支持lua脚本;
  2. Redis3.0,支持集群;
  3. Redis4.0,混合持久化,多线程异步删除;
  4. Redis5.0,核心代码重构;
  5. Redis6.0,多线程IO;
  6. Redis7.0,Function、Multi-part-AOF;

二、Redis4.0之前为什么一直采用单线程?

1、Redis采用单线程模型方便开发和维护;

2、单线程模型也可以通过IO多路复用和非阻塞IO并发处理多客户端请求;

3、对于Redis来说,主要的性能瓶颈是内存和网络,而不是CPU;

三、Redis6.0引入多线程

Redis一直是单线程架构,只不过在数据删除、数据持久化的时候使用的是多线程。但是,从网络IO处理到实际的读写命令处理,都是单线程的。

Redis的性能瓶颈主要是网络IO,因此,Redis6.0开始,采用多个IO线程来处理网络请求,提高网络请求处理的并行度。

四、Redis主线程和IO线程是如何完成请求的?

1、服务端和客户端建立socket连接

主线程负责建立连接,并把socket放入全局等待队列,主线程通过轮询的方法将socket连接分配给IO线程。

2、IO线程读取并解析请求

主线程一旦把socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求,此时,采用多个IO线程并行处理。

3、主线程执行请求命令

IO线程解析完请求,主线程还是会以单线程的方式执行这些命令。

4、IO线程会写回socket和主线程清空全局队列

当主线程执行完请求命令后,会将结果写入缓冲区,主线程进入阻塞状态,等待IO线程将结果回写到socket中,并返回给客户端。回写socket完毕后,主线程清空全局队列。

五、IO多路复用是什么?

IO多路复用,一种同步的IO模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪就能够通知到对用的应用程序进行对应的读写操作,没有文件句柄就绪时,程序就会进入阻塞状态,释放CPU资源。

  1. IO,操作系统层面指数据在内核态和用户态之间进行的读写操作;
  2. 多路,多个客户端socket连接;
  3. 复用,复用线程;
  4. IO多路复用,使用单线程就能够同时处理多个客户端socket连接;

客户端socket对应的文件描述符FileDescriptor注册进epoll,epoll会监听哪些socket有消息,避免大量的无用操作。

此时socket采用非阻塞模式,整个过程只在调用select、poll、epoll时才会阻塞,收到客户端消息不会阻塞,这个进程就会被充分利用起来,这种模式一般被称为事件驱动,也就是reactor反应模式。

采用epoll的方式,最终目的是提高服务器的吞吐能力。

IO多路复用与epoll函数才是**“Redis为什么这么快?”**的直接原因。

六、总结

Redis是一个基于内存操作、KV形式的数据库,采取多路复用、非阻塞IO、避免了不必要的上下文切换等特性。

Redis一直存在BigKey问题,因此在Redis4.0引入了多线程异步删除,正式打开Redis多线程新篇章。

Redis6.0引入IO多线程的读写,更高效的处理请求,Redis只是将IO读写变成了多线程,命令的执行还是由主线程单线程执行,因此,多线程下操作Redis不会出现线程安全的问题,不用像Java那样加锁,解锁,这也是Redis为什么这么快的根本原因。

Java学习路线总结,搬砖工逆袭Java架构师

10万字208道Java经典面试题总结(附答案)

Java基础教程系列

Java基础教程系列(进阶篇)

七、ZooKeeper+Dubbo3分布式高性能RPC通信

1、作者简介

高洪岩,某世界500强公司项目经理,有10年Java开发和项目管理经验,精通Java语言,擅长JavaEE、分布式、微服务、高性能服务器架构、智能报表、多线程和高并发相关的技术内容,理论与实践经验颇丰,也积极参与开源项目的开发与设计,涉及Dubbo、Jedis、Pulsar、ZooKeeper等主流开源项目。

著有《Java多线程编程核心技术》《Java并发编程:核心方法与框架》《NIO与Socket编程技术指南》《Java EE核心框架实战(第2版)》《Java Web实操》《虚拟化高性能NoSQL存储案例精粹:Redis+Docker》等多本图书。

2、内容简介

本教程详细介绍了ZooKeeper+ Dubbo 3联合开发时的高频实战技能,包含ZooKeeper的数据模型、Watch观察者机制、服务器角色、领导选举、ZAB协议、ZooKeeper架构、节点类型、ZooKeeper运用场景、搭建单机和主从环境、常用的Command命令、ACL授权、配额等高频使用技术点。

在Dubbo 3章节中详细介绍了单体/水平集群/垂直集群/SOA架构的发展历程、CAP理论、Dubbo特性、RPC原理、Dubbo中的五大核心组件、直连提供者、隐式参数、服务分组、多版本、启动时检查、令牌验证、超时和线程池大小、Nacos注册中心、服务提供者集群、集群容错、负载均衡等实用技能。

以上是关于Redis的IO多路复用原理的主要内容,如果未能解决你的问题,请参考以下文章

Redis深度历险:核心原理和技术实现(原理篇)

Redis深度历险:核心原理和技术实现(原理篇)

redis的多路复用原理

Redis原理篇之网络模型

redis-核心原理

Redis03——Redis之单线程+多路IO复用技术