zoukankan      html  css  js  c++  java
  • redis单线程性能高的原因

    参考:

    https://blog.csdn.net/weixin_37356262/article/details/88827688

    https://blog.csdn.net/XingXing_Java/article/details/92626174

    为什么Redis是单线程的,并且速度还非常快!

    一、前言

    近乎所有与Java相关的面试都会问到缓存的问题,基础一点的会问到什么是“二八定律”、什么是“热数据和冷数据”,复杂一点的会问到缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题,这些看似不常见的概念,都与我们的缓存服务器相关,一般常用的缓存服务器有Redis、Memcached等,而笔者目前最常用的也只有Redis这一种。

    如果你在以前面试的时候还没有遇到过面试官问你《为什么说Redis是单线程的以及Redis为什么这么快!》,那么你看到这篇文章的时候,你应该觉得是一件很幸运的事情!如果你刚好是一位高逼格的面试官,你也可以拿这道题去面试对面“望穿秋水”般的小伙伴,测试一下他的掌握程度。

    好啦!步入正题!我们先探讨一下Redis是什么,Redis为什么这么快、然后在探讨一下为什么Redis是单线程的?

    二、Redis简介

    Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库、缓存和消息中间件。

    它支持多种类型的数据结构,如字符串(Strings),散列(Hash),列表(List),集合(Set),有序集合(Sorted Set或者是ZSet)与范围查询,Bitmaps,Hyperloglogs 和地理空间(Geospatial)索引半径查询。其中常见的数据结构类型有:String、List、Set、Hash、ZSet这5种。

    Redis 内置了复制(Replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(Transactions) 和不同级别的磁盘持久化(Persistence),并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。

    Redis也提供了持久化的选项,这些选项可以让用户将自己的数据保存到磁盘上面进行存储。根据实际情况,可以每隔一定时间将数据集导出到磁盘(快照),或者追加到命令日志中(AOF只追加文件),他会在执行写命令时,将被执行的写命令复制到硬盘里面。您也可以关闭持久化功能,将Redis作为一个高效的网络的缓存数据功能使用。

    Redis不使用表,他的数据库不会预定义或者强制去要求用户对Redis存储的不同数据进行关联。

    数据库的工作模式按存储方式可分为:硬盘数据库和内存数据库。Redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快。

    (1)硬盘数据库的工作模式: 
    这里写图片描述 
    (2)内存数据库的工作模式: 
    这里写图片描述

    看完上述的描述,对于一些常见的Redis相关的面试题,是否有所认识了,例如:什么是Redis、Redis常见的数据结构类型有哪些、Redis是如何进行持久化的等。

    三、Redis到底有多快

    Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差!有兴趣的可以参考官方的基准程序测试《How fast is Redis?》(https://redis.io/topics/benchmarks

    这里写图片描述 
    横轴是连接数,纵轴是QPS。此时,这张图反映了一个数量级,希望大家在面试的时候可以正确的描述出来,不要问你的时候,你回答的数量级相差甚远!

    四、Redis为什么这么快

    1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

    2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

    3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

    4、使用多路I/O复用模型,非阻塞IO;

    5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

    以上几点都比较好理解,下边我们针对多路 I/O 复用模型进行简单的探讨:

    (1)多路 I/O 复用模型

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

    这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

    五、那么为什么Redis是单线程的

    我们首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。

    这里写图片描述 
    可以参考:https://redis.io/topics/faq

    看到这里,你可能会气哭!本以为会有什么重大的技术要点才使得Redis使用单线程就可以这么快,没想到就是一句官方看似糊弄我们的回答!但是,我们已经可以很清楚的解释了为什么Redis这么快,并且正是由于在单线程模式的情况下已经很快了,就没有必要在使用多线程了!

    但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个Redis 实例来完善!

    警告1:这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行(具体是子线程还是子进程待读者深入研究);例如我在测试服务器上查看Redis进程,然后找到该进程下的线程:

    这里写图片描述

    ps命令的“-T”参数表示显示线程(Show threads, possibly with SPID column.)“SID”栏表示线程ID,而“CMD”栏则显示了线程名称。

    警告2:在上图中FAQ中的最后一段,表述了从Redis 4.0版本开始会支持多线程的方式,但是,只是在某一些操作上进行多线程的操作!所以该篇文章在以后的版本中是否还是单线程的方式需要读者考证!

    六、注意点

    1、我们知道Redis是用”单线程-多路复用IO模型”来实现高性能的内存数据服务的,这种机制避免了使用锁,但是同时这种机制在进行sunion之类的比较耗时的命令时会使redis的并发下降。因为是单一线程,所以同一时刻只有一个操作在进行,所以,耗时的命令会导致并发的下降,不只是读并发,写并发也会下降。而单一线程也只能用到一个CPU核心,所以可以在同一个多核的服务器中,可以启动多个实例,组成master-master或者master-slave的形式,耗时的读命令可以完全在slave进行。

    需要改的redis.conf项:

     
    1. pidfile /var/run/redis/redis_6377.pid #pidfile要加上端口号

    2. port 6377 #这个是必须改的

    3. logfile /var/log/redis/redis_6377.log #logfile的名称也加上端口号

    4. dbfilename dump_6377.rdb #rdbfile也加上端口号

    2、“我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以手动地为其分配CPU核,而不会过多地占用CPU,或是让我们关键进程和一堆别的进程挤在一起。”。 
    CPU 是一个重要的影响因素,由于是单线程模型,Redis 更喜欢大缓存快速 CPU, 而不是多核

    在多核 CPU 服务器上面,Redis 的性能还依赖NUMA 配置和处理器绑定位置。最明显的影响是 redis-benchmark 会随机使用CPU内核。为了获得精准的结果,需要使用固定处理器工具(在 Linux 上可以使用 taskset)。最有效的办法是将客户端和服务端分离到两个不同的 CPU 来高校使用三级缓存。

    七、扩展

    以下也是你应该知道的几种模型,祝你的面试一臂之力!

    1、单进程多线程模型:MySQL、Memcached、Oracle(Windows版本);

    2、多进程模型:Oracle(Linux版本);

    3、Nginx有两类进程,一类称为Master进程(相当于管理进程),另一类称为Worker进程(实际工作进程)。启动方式有两种:

    (1)单进程启动:此时系统中仅有一个进程,该进程既充当Master进程的角色,也充当Worker进程的角色。

    (2)多进程启动:此时系统有且仅有一个Master进程,至少有一个Worker进程工作。

    (3)Master进程主要进行一些全局性的初始化工作和管理Worker的工作;事件处理是在Worker中进行的。

    这里写图片描述

    Redis为什么是单线程还这么快?

    Redis的高并发和快速原因

    1.redis是基于内存的,内存的读写速度非常快;

    2.redis是单线程的,省去了很多上下文切换线程的时间;

    3.redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。

    下面重点介绍单线程设计和IO多路复用核心设计快的原因。
    为什么Redis是单线程的

    1.官方答案

    因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

    2.性能指标

    关于redis的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。

    3.详细原因

    1)不需要各种锁的性能消耗

    Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除

    一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。

    总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

    2)单线程多进程集群方案

    单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。

    所以单线程、多进程的集群不失为一个时髦的解决方案。

    3)CPU消耗

    采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。

    但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?

    可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。

    Redis单线程的优劣势

    1.单进程单线程优势

    代码更清晰,处理逻辑更简单
    不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
    不存在多进程或者多线程导致的切换而消耗CPU

    2.单进程单线程弊端

    无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;
    IO多路复用技术

    redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。

    多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。

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

    I/O

    复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
    在这里插入图片描述

    Redis高并发快总结

    1. Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。

    2. 再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。

    3. Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。

    4. 另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。

    5. 还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。

    福利

    大家还可以看看《京东Java十年老鸟,总结这些年工作、跳槽、面试之经历》
    https://blog.csdn.net/XingXing_Java/article/details/93008104

    I/O多路复用技术(multiplexing)是什么?

    作者:柴小喵
    链接:https://www.zhihu.com/question/28594409/answer/52835876
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    下面举一个例子,模拟一个tcp服务器处理30个客户socket。
    假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:

    1. 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。
    这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
    2. 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
    3. 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。 
    这种就是IO复用模型,Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式
    这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。
     
     
     
     

    Redis I/O 多路复用

    引出IO多路复用

    为什么 Redis 中要使用 I/O 多路复用这种技术呢?

    首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

    要弄清问题先要知道问题的出现原因

    由于进程的执行过程是线性的(也就是顺序执行),当我们调用低速系统I/O(read,write,accept等等),进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作。阻塞很正常, 接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服
    务器端read(sockfd1,bud,bufsize),此时客户端进程没有发送数据,那么read(阻塞调用)将阻塞直到客户端write(sockfd,but,size)发来数据。在一个客户和服务器通信时这没什么问题,当多个客户与服务器通信时,若服务器阻塞于其中一个客户sockfd1,当另一个客户的数据到达套接字sockfd2时,服务器仍不能处理,仍然阻塞在read(sockfd1,...)上。此时问题就出现了,不能及时处理另一个客户的服务,肿么办?I/O多路复用来解决!

    继续上面的问题,有多个客户连接,sockfd1、sockfd2、sockfd3..sockfdn同时监听这n个客户,当其中有一个发来消息时就从select的阻塞中返回,然后就调用read读取收到消息的sockfd,然后又循环回select阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息。

    Q:
    那这样子,在读取socket1的数据时,如果其它socket有数据来,那么也要等到socket1读取完了才能继续读取其它socket的数据吧。那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程I/O有什么区别呢?

    A:
    1.CPU本来就是线性的,不论什么都需要顺序处理,并行只能是多核CPU。

    2.I/O多路复用本来就是用来解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题,跟多线程没关系。

    3.跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资源。而I/O多路复用不需要切换线/进程,效率相对较高,特别是对高并发的应用nginx就是用I/O多路复用,故而性能极佳。但多线程编程逻辑和处理上比I/O多路复用简单,而I/O多路复用处理起来较为复杂。

    理解IO多路复用

    什么是I/O 多路复用

    I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。结合下图可以清晰地理解I/O多路复用。

    select, poll, epoll 都是I/O多路复用的具体的实现。epoll性能比其他几者要好。redis中的I/O多路复用的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。

    多路分离函数select

    IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

    如上图所示,用户线程发起请求的时候,首先会将socket添加到select中,这时阻塞等待select函数返回。当数据到达时,select被激活,select函数返回,此时用户线程才正式发起read请求,读取数据并继续执行。

    从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    Reactor(反应器模式)

    如上图,I/O多路复用模型使用了Reactor设计模式实现了这一机制。通过Reactor的方式,可以将用户线程轮询I/O操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路I/O复用模型也被称为异步阻塞I/O模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用I/O多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起I/O请求时,数据已经到达了,用户线程一定不会被阻塞。

    总结

    I/O 多路复用模型是利用select、poll、epoll可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll是只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

    参考:

    https://baijiahao.baidu.com/s?id=1624003934114185747&wfr=spider&for=pc

    https://www.cnblogs.com/syyong/p/6231326.html

    https://blog.csdn.net/happy_wu/article/details/80052617

    https://blog.csdn.net/tanswer_/article/details/70196139

    一文读懂I/O多路复用技术

    前言

    当我们要编写一个echo服务器程序的时候,需要对用户从标准输入键入的交互命令做出响应。在这种情况下,服务器必须响应两个相互独立的I/O事件:1)网络客户端发起网络连接请求,2)用户在键盘上键入命令行。我们先等待哪个事件呢?没有哪个选择是理想的。如果在acceptor中等待一个连接请求,我们就不能响应输入的命令。类似地,如果在read中等待一个输入命令,我们就不能响应任何连接请求。针对这种困境的一个解决办法就是I/O多路复用技术。基本思路就是使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。--《UNIX网络编程》
    我们以书中的这段描述来引出我们要讲述的I/O多路复用技术。

    I/O多路复用概述


    I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接。现在大部分讲述I/O多路复用的文章用到的上面这张图是《UNIX网络编程》一书的。那么这也是当前我们理解I/O多路复用技术的基础知识。从这张图里面我们GET到哪些点呢?
    个人理解有:
    1、怎么区分的应用进程与内核
    2、有两次系统调用分别是select和recvfrom
    3、两次系统调用进程都阻塞
    4、等待哪些数据准备好
    下面我们逐一阐述。

    二、用户进程和内核

    根据网络OSI七层模型和网际网协议族的同比,我们可以知道这里说的用户进程和内核是以传输层为分割线,传输层以上(不包括)是指用户进程,传输层以下(包括)是指内核。上三层,web客户端比如浏览器、web服务器这些都属于应用层,里面跑的程序则是应用进程。下四层处理所有的通信细节,发送数据,等待确认,给无序到达的数据排序等等。这四层也是通常作为操作系统内核的一部分提供。由此可见图1中说的系统调用的地方正是第四层和第五层之间的位置。

    为了理解用户进程和内核,再来看一张图,网络数据流向图。也清晰的标明了用户进程和内核的位置。值得注意的一点是客户与服务器之间的信息流在其中一端是向下通过协议栈的,跨越网络后,在另一端是向上通过协议栈的。这张图描述的是局域网内,如果是在广域网那么就是通过很多个路由器承载实际数据流。

    三、select和recvfrom

    3.1、select

    理解了select就抓住了I/O多路复用的精髓,对应的操作系统中调用的则是系统的select函数,该函数会等待多个I/O事件(比如读就绪,写)的任何一个发生,并且只要有一个网络事件发生,select线程就会执行。如果没有任何一个事件发生则阻塞。我们在下面小节中会重点讲述。函数如下:

    1.  
      #include<sys/select.h>
    2.  
      #include<sys/time.h>
    3.  
      int select(int maxfdpl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

    从这个函数的定义中的参数,我们能够看出它描述的是,当调用select的时候告知内核对那些事件(读就绪,写)感兴趣以及等待多长时间。

    为了方便我们理解select调用,可以参照下面这张图,是jdk的基于I/O多路复用技术的NIO实现。重点在于理解Selector复用器。

    大致代码如下:

    ServerSocketChannel serverChannel = ServerSocketChannel.open();// 打开一个未绑定的serversocketchannel   
    Selector selector = Selector.open();// 创建一个Selector
    serverChannel .configureBlocking(false);//设置非阻塞模式
    serverChannel .register(selector, SelectionKey.OP_READ);//将ServerSocketChannel注册到Selector
     
     
    while(true) {
      int readyChannels = selector.select();
      if(readyChannels == 0) continue;
      Set selectedKeys = selector.selectedKeys();
      Iterator keyIterator = selectedKeys.iterator();
      while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
            // a connection was accepted by a ServerSocketChannel.
        } else if (key.isConnectable()) {//连接就绪
            // a connection was established with a remote server.
        } else if (key.isReadable()) {//读就绪
            // a channel is ready for reading
        } else if (key.isWritable()) {//写就绪
            // a channel is ready for writing
        }
        keyIterator.remove();
      }
    }


     

    3.2、recvfrom

    recvfrom一般用于UDP协议中,但是如果在TCP中connect函数调用后也可以用。用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。也就是我们本文中以及书中说的真正的I/O操作。

    四、阻塞、非阻塞

    这张图可以看出阻塞式I/O、非阻塞式I/O、I/O复用、信号驱动式I/O他们的第二阶段都相同,也就是都会阻塞到recvfrom调用上面就是图中“发起”的动作。异步式I/O两个阶段都要处理。这里我们重点对比阻塞式I/O(也就是我们常说的传统的BIO)和I/O复用之间的区别。

    阻塞式I/O和I/O复用,两个阶段都阻塞,那区别在哪里呢?就在于第三节讲述的Selector,虽然第一阶段都是阻塞,但是阻塞式I/O如果要接收更多的连接,就必须创建更多的线程。I/O复用模式下在第一个阶段大量的连接统统都可以过来直接注册到Selector复用器上面,同时只要单个或者少量的线程来循环处理这些连接事件就可以了,一旦达到“就绪”的条件,就可以立即执行真正的I/O操作。这就是I/O复用与传统的阻塞式I/O最大的不同。也正是I/O复用的精髓所在。

    从应用进程的角度去理解始终是阻塞的,等待数据和将数据复制到用户进程这两个阶段都是阻塞的。这一点我们从应用程序是可以清楚的得知,比如我们调用一个以I/O复用为基础的NIO应用服务。调用端是一直阻塞等待返回结果的。
    从内核的角度等待Selector上面的网络事件就绪,是阻塞的,如果没有任何一个网络事件就绪则一直等待直到有一个或者多个网络事件就绪。但是从内核的角度考虑,有一点是不阻塞的,就是复制数据,因为内核不用等待,当有就绪条件满足的时候,它直接复制,其余时间在处理别的就绪的条件。这也是大家一直说的非阻塞I/O。实际上是就是指的这个地方的非阻塞。

    当我们阅读《UNIX网络编程》(第三版)一书的时候。P124,6.2.3小节中“而不是阻塞在真正的I/O系统调用上”这里的阻塞是相对内核来说的。P127,6.2.7小节“因为其中真正的I/O操作(recvfrom)将阻塞进程”这里的阻塞是相对用户进程来说的。明白了这两点,理解起来就不矛盾了,而且一通到底!

    五、适用场景

    当服务程序需要承载大量TCP链接的时候,比如我们的消息推送系统,IM通讯,web聊天等等,在我们已经理解Selector原理的情况下,知道使用I/O复用可以用少量的线程处理大量的链接。I/O多路复用技术以事件驱动编程为基础。它运行在单一进程上下文中,因此每个逻辑流都能访问该进程的全部地址空间,这样在流之间共享数据变得很容易。

    六、总结

    我们通常说的NIO大多数场景下都是基于I/O复用技术的NIO,比如jdk中的NIO,当然Tomcat8以后的NIO也是指的基于I/O复用的NIO。注意,使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。如果放到线上环境,网络情况在有时候并不稳定的情况下,这种基于I/O复用技术的NIO的优势就是传统BIO不可同比的了。那么使用select的优势在于我们可以等到网络事件就绪,那么用少量的线程去轮询Selector上面注册的事件,不就绪的不处理,就绪的拿出来立即执行真正的I/O操作。这个使得我们就可以用极少量的线程去HOLD住大量的连接。

    转载请注明作者及出处,并附上链接https://blog.csdn.net/wangxindong11/article/details/78591308

    参考资料:
    《UNIX网络编程》、《深入理解计算机操作系统》

  • 相关阅读:
    解决后退网页已过期或刷新询问是否重新提交表单的问题
    一行代码获取中文星期
    单例模式弹出窗体实现
    JAVA实现冒泡排序
    关于BufferedWriter.write超过30W条数据写入过慢问题。
    Ibatis的简单介绍
    链接注入(便于跨站请求伪造)(AppScan扫描结果)
    会话标识未更新(AppScan扫描结果)
    跨站点脚本编制实例(AppScan扫描结果)
    深入Java核心 Java内存分配原理精讲
  • 原文地址:https://www.cnblogs.com/xuwc/p/14015580.html
Copyright © 2011-2022 走看看