阿里巴巴面试题总结(java后端)-第二章

③,三面 1,介绍你实践的性能优化例子,以及你优化的思路 结合自己在做的具体业务,同步变异步(无强依赖的服务调用改成并发调用),异地缓存多活,异地双主mysql,hystrix熔断,限流机制,99线统计,热点数据缓存化等等。

2,微服务和soa的区别,优劣势?

SOA:微服务是细粒度的SOA(面向服务架构),一个SOA组件可以拆分成多个微服务,SOA的服务架构依赖ESB企业服务总线(专注应用程序服务的可重用性的最大化)。 微服务:微服务架构是一个大的系统按照功能拆分成很多独立的子服务,每个子服务独立实现自己的业务逻辑,各个微服务之间的关联通过暴露api来实现。这些独立的微服务不需要部署在同一个虚拟机,同一个系统和同一个应用服务器中(微服务架构专注于解耦)。

SOA架构的优点 <1>系统松耦合,提高传统企业的功能模块的可重用性。 <2>决企业系统间的通信问题,把原先散乱、无规划的系统间的网状结构,梳理成 规整、可治理的系统间星形结构。

SOA架构的缺点 <1>严重依赖比较重的ESB企业服务总线,企业总线出了问题,就导致整个架构的不可能。 <2>服务粗化,比如读写负载不同的功能耦合在一个服务里面。

微服务架构模式优点: <1>由于每个服务都是独立并且微小的,由单独的团队负责,采用敏捷开发模式,迭代效率很高。 <2>每一个微服务都是独立部署的,可根据当前服务的负载程度选择不同配置的机器。

微服务架构的缺点: <1>微服务应用作为分布式系统带来了复杂性。各个微服务进行分布式独立部署,由不同的技术团队维护,因此经常出现跨团队沟通,效率容易不可控。 <2>微服务架构一般使用各个独立数据库,分布式事务的实现更具挑战性。 <3>测试微服务变得复杂,通常一个服务会依赖很多服务,测试环境下其他服务的不可靠,容易影响测试进度。

备注:企业服务总线(ESB)就是一条企业架构的总线,所有的企业服务都挂接到该总线上对外公布,企业服务总线负责管理服务目录,解析服务请求者的请求方法、消息格式,并对服务提供者进行寻址,转发服务请求

3,sql慢查询的优化方案,索引和表的优化方案 慢查询优化方案: <0>explain检查sql语句是否索引覆盖 <1>能使用主键查询的尽量使用主键查询 <2>连接查询代(join on)替子查询(in)和联合查询(from t1,t2),避免在内存从形成巨大的笛卡尔积,多表连接时,尽量小表驱动大表,即小表join大表 <3>批量扫表时,每次查询应该使用上次查询返回的主键id作为下一轮的查询条件,提高查询效率。 <4>组合索引使用应该满足最左原则。 <5>数据量比较的时候,尽量使用limit进行物理分页查询 <6>查询时,返回结果能不要就不用,尽量写全字段名

索引优化: <1>只要列中含有NULL值,就最好不要在此例设置索引,复合索引如果有NULL值,此列在使用时也不会使用索引 <2>尽量使用短索引,索引字段不易过长 <3>尽量不要使用not in和<>操作 <4>对于like语句,以%或者‘-’开头的不会使用索引,以%结尾会使用索引 <5>尽量不要在列上进行运算(函数操作和表达式操作) <6>字段范围很窄(比如只有1,2,3种可能),就没必要建立索引,对查询效率没有太大的提升。

表的优化: <1>表的字段尽可能用NOT NULL <2>字段长度固定的表查询会更快 <3>数据量超过2000w级别的,可以考虑分表

4,Mysql和MongoDB的区别,海量数据量如何存储? Mysql是关系型数据库,MongoDB是非关系型文档数据库。

备注:MongoDB将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

<1>Mysql:无论数据还是索引都存放在硬盘中。到要使用的时候才交换到内存中,能够处理远超过内存总量的数据,单表单库不适合存储海量数据。

<2>MongoDB,虚拟内存+持久化,在适量级的内存的 MongoDB 的性能是非常迅速的,它将热数据存储在物理内存中,使得热数据的读写变得十分快,MongoDB 的所有数据实际上是存放在硬盘的,所有要操作的数据通过mmap的方式映射到内存某个区域内。然后MongoDB 就在这块区域里面进行数据修改,避免了零碎的硬盘操作。 MongoDB的索引放在内存中,能够提升随机读写的性能。如果索引不能完全放在内存,一旦出现随机读写比较高的时候,就会频繁地进行磁盘交换,MongoDB 的性能就会急剧下降(MongoDB不支持事务)

海量数据处理:MongoDB 以BSON结构(二进制)进行存储,对海量数据存储有着很明显的优势,支持服务端脚本和Map/Reduce,可以实现海量数据计算,即实现云计算功能。

参考:blog.csdn.net/CatStarXcod…

5,缓存框架,例如redis和memcache之间的区别,优劣势 <1>存储策略:memcached超过内存比例会抹掉前面的数据,而redis拒绝写入内存。 <2>支持数据类型:memcached只支持string,redis支持更多。如:hash,list,set,sorted。 <3>redis支持两种持久化策略(rdb,aof),memcached <4>灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过rdb、aof方式恢复。 <5>memcache是单进程多线程,redis是单线程的。 <6>Memcached单个key-value大小有限,一个value最大只支持1MB,而Redis最大支持512MB <7>分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从。 <8>memcache是两阶段hash(第一次是一致性hash先找集群中的实例,第二次是找kv数据),redis是通过crc16算法计算出slot中的位置,再通过hash查找出数据。

6,请描述一下一致性hash算法 具体算法过程为:先构造一个长度为232次方的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232次方-1])将缓存服务器节点放置在这个Hash环上,然后根据需要缓存的数据的Key值计算得到其Hash值(其分布也为[0, 232-1]),然后在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。 举例:三个Node点分别位于Hash环上的三个位置,然后Key值根据其HashCode,在Hash环上有一个固定位置,位置固定下之后,Key就会顺时针去寻找离它最近的一个Node,把数据存储在这个Node的Cache(如memcache)服务器中。

一致性hash算法解决的问题:主要是考虑到分布式系统每个节点都有可能失效,或者新的节点很可能动态的增加进来的情况,我们只需要移动最少的数据,就可以保证数据的均匀分配 一致性hash算法,减少了数据映射关系的变动,不会像hash(i)%N那样带来全局的变动 普通的余数hash,分流时机器宕机会产生失败请求,容易引起请求丢失。即使mod值(hash取模值)变化,如果是redis集群,要重新移动key进行分配,数据迁移 普通的余数hash(hash(比如用户id)%服务器机器数)算法伸缩性很差,当新增或者下线服务器机器时候,用户id与服务器的映射关系会大量失效。 一致性hash环的数据倾斜问题:一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题

参考:www.cnblogs.com/lpfuture/p/…

7,分布式session的共享方案有哪些,有什么优劣势 背景:Session是服务器用来保存用户操作的一系列会话信息,由Web容器进行管理。单机情况下,不存在Session共享的情况,分布式情况下,如果不进行Session共享会出现请求落到不同机器要重复登录的情况,一般来说解决Session共享有以下几种方案。

<1>、session复制 session复制是早期的企业级的使用比较多的一种服务器集群session管理机制。应用服务器开启web容器的session复制功能,在集群中的几台服务器之间同步session对象,使得每台服务器上都保存所有的session信息(不同功能应用的机器保存了其它应用的session),这样任何一台宕机都不会导致session的数据丢失,服务器使用session时,直接从本地获取。

缺点:应用集群变的庞大以后,就会出现瓶颈,每台都需要备份session,占用的空间变的非常大,出现内存不够用的情况。

<2>,利用cookie记录session session记录在客户端,每次请求服务器的时候,将session放在请求中发送给服务器,服务器处理完请求后再将修改后的session响应给客户端。这里的客户端就是cookie。

缺点:受cookie大小的限制,能记录的信息有限;每次请求响应都需要传递cookie,影响性能,如果用户关闭cookie,访问就不正常。

<3>session持久化(mysql) 缺点:不适合高并发的场景,mysql读取性能受限。

<4>session绑定 利用hash算法,比如nginx的ip_hash,使得同一个ip的请求分发到同一台服务器上。 缺点:这种方式不符合对系统的高可用要求,因为一旦某台服务器宕机,那么该机器上的session也就不复存在了,用户请求切换到其他机器后么有session,无法完成业务处理。

参考:www.jianshu.com/p/221f8a42b…

<5>session服务器 session服务器(基于redis、memcache存储)可以解决上面的所有的问题,利用独立部署的session服务器(集群)统一管理session,服务器每次读写session时,都访问session服务器(需要我们实现session集群服务的高可用)

8,高并发情况,系统的优化方案有哪些?以及优先级排序

④,其它题目补充 1,自旋锁和偏向锁 背景:并发编程中synchronized是重量级锁,但随着JVM1.6对synchronized进行优化后,有些情况下它并不那么重,实际上性能不亚于lock。 Synchronized的锁升级过程:偏向锁--》轻量级锁--》重量级锁 整个过程是单向的,不支持降级。

简单理解:单线程(thread1)状态是使用偏向锁,thread1线程在执行过程中,别的线程(thread2)过来,发现锁处于锁处于偏向锁状态,thread2会把锁改成轻量级锁,此时thread2进入自旋状态(类似while的死循环),并且不释放cpu直至等待到锁,实际上自旋状态有超时限制的(避免长时间消耗cpu)。thread2运行过程中,thread3过来获取锁,发现锁处于轻量级锁状态,会升级成重量级锁(这种场景一般都是处于高并发状态了)。

偏向锁 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁(通过cas机制)时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入(锁重入)和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。 注意:当锁有竞争关系的时候,需要解除偏向锁,进入轻量级锁。

轻量级锁 <1>加锁 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 <2>解锁 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

自旋锁 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间(最大自旋次数)。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

参考:www.jianshu.com/p/1ea87c152… 补充:关于cas,cmpxchg的底层讲解,参考:www.cnblogs.com/luconsole/p…

2,volatile关键字 <1>内存可见性,线程修改完变量数据后,会立马写入到共享内存中,其它线程若读取该变量,会强制从共享内存中获取,但是不能保证线程安全(已经加载进入栈空间的变量,只有再次读取,才会从共享内存取数据),volatile修饰的变量,在写入共享内存的时候,是使用cpu的lock指令,这个过程是lock内核总线,保证数据的安全性。 <2>防止指令重排,编译阶段,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序(普通的代码程序,编译器在编译期对没有前后依赖关系的代码做一些重排优化)。

3,Synchronized,volatile,reentrantlock的底层实现存在的关联 <1>Synchronized在获取同步监视器的时候,是使用cas机制修改对象头,cas机制在cpu实现上使用lock(cmpxchg)指令锁住总线。 <2>volatile在数据写入共享内存时也使用lock指令锁住总线,保证共享内存数据可见性。 <3>reentrantlock实现使用了AQS,AQS基于cas机制实现,底层也使用了lock指令。

3,什么是数组和链表?什么情况下使用二者

其它面试题: 1,mp.weixin.qq.com/s/Y2lxsucvk…? 2,mp.weixin.qq.com/s/bc4cc6OUE… 3,mp.weixin.qq.com/s/TarTEBF3N…? 4,mp.weixin.qq.com/s/aPSOH1VoL…

其它知识点总结 1,什么是缓存击穿? 缓存是加速系统响应的一种途径,通常情况下只有系统的部分数据。当请求了缓存中没有的数据时,这时候就会回源到DB里面。此时如果黑客故意对上面数据发起大量请求,则DB有可能会挂掉,这就是缓存击穿。当然缓存挂掉的话,正常的用户请求也有可能造成缓存击穿的效果。(实际应用中经常遇到第三方爬虫,也容易导致缓存击穿) 解决缓存击穿的方案:布隆过滤器 bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。 和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。

补充:缓存穿透前面加一层布隆过滤器,缓存雪崩访问本地兜底数据,如果兜底数据没有命中,转发流量到mysql(限流,实际上没命中热点兜底数据的流量很低),热点数据集中失效(可以对有访问的数据进行失效时间延长续命) 参考:juejin.im/post/5c9a67…

算法:

  1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数
  2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
  3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
  4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。 优点:不需要存储key,节省空间

2,netty的Reactor模式为什么使用多线程模型?(参考:blog.csdn.net/xiaolang85/…)

<1>单线程模型存在的问题:

由于Reactor模式使用的是同步非阻塞IO,所有的IO操作都不会导致阻塞,理论上一个线程可以独立处理所有IO相关的操作。从架构层面看,一个NIO线程确实可以完成其承担的职责。例如,通过Acceptor类接收客户端的TCP连接请求消息,链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户线程可以通过消息编码通过NIO线程将消息发送给客户端。 对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下: 1)一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送; 2)当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈; 3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

备注:比如一次netty访问一个网络io,返回的数据是二进制流,此时如果让selector线程(实际是netty中的boss和worker线程)负责反序列化,就会阻塞selector,无法接受更多的连接,这种模型明显不合理的。

<2>Reactor多线程模型 Reactor多线程模型的特点: 1)有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求; 2)网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送; 3)1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程(1个NIO线程可以处理所有的链路) 在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

<3>主从多线程模型 主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。

利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。 它的工作流程总结如下: 从主线程池中随机选择一个Reactor线程作为Acceptor线程,用于绑定监听端口,接收客户端连接; Acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作; 步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,用于处理I/O的读写操作。

<4>服务端线程模型 一种比较流行的做法是服务端监听线程和IO线程分离,类似于Reactor的多线程模型(Netty同时支持Reactor的单线程、多线程和主从多线程模型,在不同的应用中通过启动参数的配置来启动不同的线程模型) 第一步,从用户线程发起创建服务端操作 通常情况下,服务端的创建是在用户进程启动的时候进行,因此一般由Main函数或者启动类负责创建,服务端的创建由业务线程负责完成。在创建服务端的时候实例化了2个EventLoopGroup,1个EventLoopGroup实际就是一个EventLoop线程组,负责管理EventLoop的申请和释放。 EventLoopGroup管理的线程数可以通过构造函数设置,如果没有设置,默认取-Dio.netty.eventLoopThreads,如果该系统参数也没有指定,则为可用的CPU内核数 × 2。 bossGroup线程组实际就是Acceptor线程池,负责处理客户端的TCP连接请求,如果系统只有一个服务端端口需要监听,则建议bossGroup线程组线程数设置为1。 workerGroup是真正负责I/O读写操作的线程组,通过ServerBootstrap的group方法进行设置,用于后续的Channel绑定。

第二步,Acceptor线程绑定监听端口,启动NIO服务端,相关代码如下: 从bossGroup中选择一个Acceptor线程监听服务端 其中,group()返回的就是bossGroup,它的next方法用于从线程组中获取可用线程,代码如下: 选择Acceptor线程 服务端Channel创建完成之后,将其注册到多路复用器Selector上,用于接收客户端的TCP连接,核心代码如下: 图2-5 注册ServerSocketChannel 到Selector 第三步,如果监听到客户端连接,则创建客户端SocketChannel连接,重新注册到workerGroup的IO线程上。首先看Acceptor如何处理客户端的接入: 图2-6 处理读或者连接事件 调用unsafe的read()方法,对于NioServerSocketChannel,它调用了NioMessageUnsafe的read()方法,代码如下: 图2-7 NioServerSocketChannel的read()方法 最终它会调用NioServerSocketChannel的doReadMessages方法,代码如下: 图2-8 创建客户端连接SocketChannel 其中childEventLoopGroup就是之前的workerGroup, 从中选择一个I/O线程负责网络消息的读写。 第四步,选择IO线程之后,将SocketChannel注册到多路复用器上,监听READ操作。 图2-9 监听网络读事件 第五步,处理网络的I/O读写事件,核心代码如下: 图2-10 处理读写事件

备注:Dubbo默认的底层网络通讯使用的是Netty,服务提供方NettyServer使用两级线程池,其中 EventLoopGroup(boss) 主要用来接受客户端的链接请求,并把接受的请求分发给 EventLoopGroup(worker) 来处理,boss和worker线程组我们称之为IO线程。

3,dubbo常见面试题(blog.csdn.net/Y0Q2T57s/ar… <1>dubbo为什么使用线程池 实际上是可以选择是否使用线程池,如果不使用线程池,对于一些长耗时的网络io(响应的数据比较大),selector线程(实际就是netty中的worker线程)会阻塞在处理结果(比如网络io响应的结果进行序列化),导致selector无法接受更多的请求,这些长耗时的逻辑应该下沉到业务线程池里面,与netty线程隔离开。

备注:对于长耗时的服务,实际上无论异步或者同步,都应该使用线程池,异步模式是基于nio+future实现,长耗时的网络io执行结果还是需要用到线程池去支持序列化,不应该消耗worker线程。

<2>dubbo的事件派发策略和线程池(默认使用了线程池) dubbo基于netty。有5种派发策略: 默认是all:所有消息都派发到线程池,包括请求,响应,连接事件,断开事件,心跳等。 即worker线程接收到事件后,将该事件提交到业务线程池中,自己再去处理其他事。 direct:worker线程接收到事件后,由worker执行到底。 message:只有请求响应消息派发到线程池,其它连接断开事件,心跳等消息,直接在 IO线程上执行 execution:只请求消息派发到线程池,不含响应(客户端线程池),响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行 connection:在 IO 线程上,将连接断开事件放入队列,有序逐个执行,其它消息派发到线程池。

参考:blog.csdn.net/wanbf123/ar…

<3>Dubbo提供的线程池策略 扩展接口 ThreadPool 的SPI实现有如下几种: fixed:固定大小线程池,启动时建立线程,不关闭,一直持有(缺省)。 cached:缓存线程池,空闲一分钟自动删除,需要时重建。 limited:可伸缩线程池,但池中的线程数只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然带来大流量引起性能问题

4,tcp粘包问题分析与对策(www.cnblogs.com/kex1n/p/650… TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾,出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

实际上是传输层不知道报文在哪里隔断,也就是说传输层不关注边界,应用层自己解决,因此任何基于tcp传输协议的通信框架都需要自己解决粘包问题。 备注:在流传输中出现,UDP不会出现粘包,因为它有消息边界(可以理解为每条消息)

TCP无保护消息边界的解决

针对这个问题,一般有3种解决方案(netty也是这样做): (1)发送固定长度的消息(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。 (2)把消息的尺寸与消息一块发送(消息头(消息头包含某条消息的长度)和消息体一块发送) (3)使用特殊标记来区分消息间隔

5,为什么基于TCP的通讯程序需要进行封包和拆包

TCP是个"流"协议,所谓流,就是没有界限的一串数据,大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包。由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况。

假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).

A.先接收到data1,然后接收到data2.

B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.

C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.

D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包,为了拆包就必须在发送端进行封包。

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收

6,netty针对tcp粘包的几种解决方案 <1>回车换行解码器:LineBasedFrameDecoder <2>特殊分隔符解码器:DelimiterBasedFrameDecoder,回车换行解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。 <3>定长解码器:FixedLengthFrameDecoder,对于定长消息,如果消息实际长度小于定长,则往往会进行补位操作,它在一定程度上导致了空间和资源的浪费。但是它的优点也是非常明显的,编解码比较简单,因此在实际项目中仍然有一定的应用场景 <4>基于包头不固定长度的解码器:LengthFieldBasedFrameDecoder,协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、HTTP协议等。由于基于长度解码需求的通用性,以及为了降低用户的协议开发难度,Netty提供了LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。LengthFieldBasedFrameDecoder比较灵活通用,由客户端告诉接收端,我传输的报文有多长了,接收端根据长度来解析。

7,netty面试题(blog.csdn.net/baiye_xing/…

8,Netty 中Channel、EventLoop、Thread、EventLoopGroup之间的关系 EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件。 一个EventLoopGroup包含一个或者多个EventLoop。 一个EventLoop在它的生命周期内只和一个Thread绑定。 所有由EventLoop处理的I/O事件都将在它专有的Thread上被处理。 一个Channel在它的生命周期内只注册于一个EventLoop。 一个EventLoop可能会被分配给一个或多个Channel。 在这种设计中,一个给定Channel的I/O操作都是由相同的Thread执行的。

9,NIOEventLoopGroup是怎么与Reactor关联在一起的呢? 其实NIOEventLoopGroup就是一个线程池实现,通过设置不同的NIOEventLoopGroup方式就可以对应三种不同的Reactor线程模型。 <1>单线程模型 EventLoopGroup bossGroup = new NioEventLoopGroup(1); 实例化了一个NIOEventLoopGroup,构造参数是1表示是单线程的线程池 <2>多线程模型 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(N); bossGroup 中只有一个线程, 在workerGroup线程池中我没有指定线程数量,所以默认是CPU 核心数乘以2, 因此对应的到Reactor线程模型中,这样设置的 NioEventLoopGroup 其实就是Reactor多线程模型。

<3>主从Reactor线程模型 EventLoopGroup bossGroup = new NioEventLoopGroup(4); EventLoopGroup workerGroup = new NioEventLoopGroup(N); Boss可以负责鉴权等工作

参考:blog.csdn.net/u010853261/…

10,rpc的future模式 pigeon的源码分析,对比一下源码,实际上就是netty多线程模型的底层线程池(如果不开线程,就是selector线程轮询结果)执行完毕,将结果放在当前的前程的threadLocal里面,将来调用future.get获取 参考:blog.csdn.net/ningdunquan…

11,如何利用压测工具挖掘服务的性能?(jvm调优,gc调优) <1>比如可以通过ptest进行压测,观测压测期间是否出现频繁的fullgc,如果出现,间接反映出对象的使用不太合理,比如一个逻辑只需要获取一个简单的数据,但是调用的服务返回的是一个大对象,假设调用链很长,这样就会长期占用很大的堆内存空间,因此最好根据需求(查询请求设置要求返回的字段数据)返回,不需要返回多余的数据(按需索取),提高服务的响应性能。使用堆外内存,也会导致机器剩余可分配的堆内的空间少了,但是空间小了,full gc的也会变短,因此gc不需要扫描那么多空间。

<2>本地缓存由堆内存迁移到堆外内存,避免本地缓存的数据长期占用大量的堆内存空间和导致频繁的full gc。 参考:www.cnblogs.com/andy-zhou/p…

12,深度理解select、poll和epoll select的缺点: <1>单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量(如果修改,就得要自己重新编译内核,重新安装系统,实际很多创业公司都没自己的内核工程师),但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE    1024 <2>内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销; <3>select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件,实际上这个时间是o(n),时间开销比较高(select中,当有事件就绪时,内核修改参数以通知用户,用户需要遍历所有的fd判断是哪个fd就绪,应用程序索引就绪文件描述符的时间复杂度是O(n),IO效率随着监听的fd的数目增加而线性下降。) <4>select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

poll的缺点: 相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在(不断轮询所有的句柄数组,耗时还是很长)

参考:blog.csdn.net/wendy_keepi…

epoll的特点: epoll中注册了回调函数,当有就绪事件发生的时候,设备驱动程序调用回调函数,将就绪的fd添加到就绪链表rdllist中,调用epoll_wait时,将rdllist上就绪的fd发送给用户,应用程序索引就绪文件描述符的时间复杂度是O(1),IO效率与fd的数目无关,

1)没有最大并发连接的限制,能打开FD的上限远大于1024(1G的内存上能监听约10万个端口);

2)效率提升。不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;

即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。

3)内存映射。epoll通过内核和用户空间共享一块内存来实现消息传递的。利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap 减少复制开销。epoll保证了每个fd在整个过程中只会拷贝一次(select,poll每次调用都要把fd集合从用户态往内核态拷贝一次)。

补充: epoll永远比select高效吗? 不一定! epoll适用于连接较多,活动数量较少的情况。 (1)epoll为了实现返回就绪的文件描述符,维护了一个红黑树和好多个等待队列,内核开销很大。如果此时监听了很少的文件描述符,底层的开销会得不偿失;

(2)epoll中注册了回调函数,当有时间发生时,服务器设备驱动调用回调函数将就绪的fd挂在rdllist上,如果有很多的活动,同一时间需要调用的回调函数数量太多,服务器压力太大。

select适用于连接较少的情况。 当select上监听的fd数量较少,内核通知用户现在有就绪事件发生,应用程序判断当前是哪个fd就绪所消耗的时间复杂度就会大大减小。

todo: LT:level trigger, 水平触发模式 ET:edge trigger, 边缘触发模式

14,根据OSI参考模型分为(从上到下):物理层->数据链路层->网络层->传输层->会话层->表示层->应用层。 TCP/IP层次模型共分为四层:应用层->传输层->网络层->数据链路层。 参考:www.cnblogs.com/kevingrace/…

15,spring bean生命周期 Spring框架中,一旦把一个Bean纳入Spring IOC容器之中,这个Bean的生命周期就会交由容器进行管理,一般担当管理角色的是BeanFactory或者ApplicationContext,认识一下Bean的生命周期活动,对更好的利用它有很大的帮助:

下面以BeanFactory为例,说明一个Bean的生命周期活动 Bean的建立, 由BeanFactory读取Bean定义文件,并生成各个实例 Setter注入,执行Bean的属性依赖注入 BeanNameAware的setBeanName(), 如果实现该接口,则执行其setBeanName方法 BeanFactoryAware的setBeanFactory(),如果实现该接口,则执行其setBeanFactory方法 BeanPostProcessor的processBeforeInitialization(),如果有关联的processor,则在Bean初始化之前都会执行这个实例的processBeforeInitialization()方法 InitializingBean的afterPropertiesSet(),如果实现了该接口,则执行其afterPropertiesSet()方法 Bean定义文件中定义init-method BeanPostProcessors的processAfterInitialization(),如果有关联的processor,则在Bean初始化之前都会执行这个实例的processAfterInitialization()方法 DisposableBean的destroy(),在容器关闭时,如果Bean类实现了该接口,则执行它的destroy()方法 Bean定义文件中定义destroy-method,在容器关闭时,可以在Bean定义文件中使用“destory-method”定义的方法

备注:BeanPostProcessor接口的作用:如果我们需要在Spring容器完成Bean的实例化、配置和其他的初始化前后添加一些自己的逻辑处理,我们就可以定义一个或者多个BeanPostProcessor接口的实现,然后注册到容器中去。

public class TestBeanPostProcessor implements BeanPostProcessor {

/**
 * 实例化之后进行处理
 */
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    return bean;
}

/**
 * 实例化之前进行处理
 */
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    return bean;
}
复制代码

} 前者在初始化代码调用之后调用。 后者在实例化及依赖注入完成后,在任何初始化代码(比如配置文件中的init-method)调用之前调用。(参考:www.cnblogs.com/libra0920/p…

16,类装载器ClassLoader 工作机制: 类装载器就是寻找类的字节码文件并构造出类在JVM内部表示的对象组件。在Java中,类装载器装入JVM中,要经过以下步骤: 1.装载:查找和导入Class文件 2.链接:执行校验、准备和解析步骤,其中解析步骤是可以选择的: 校验:检查载入Class文件数据的正确性(校验是否是合法的字节码文件) 准备:给类的静态变量分配存储空间 解析:将符号引用转成直接引用 3.初始化:对类的静态变量、静态代码块执行初始化工作 4,在Java堆中生成一个代表这个类的java.lang.Class对象 类加载器工作由ClassLoader及其子类负责,ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。JVM在运行时会产生三个ClassLoadre:根装载器、ExtClassLoader和AppClassLoader。根装载器不是ClassLoader的子类,负责装载JRE的核心类库。ExtClassLoader和AppClassLoader都是ClassLoader的子类。其中,EctClassLoader负责装载JRE扩展目录ext中的JAR类包,AppClassLoader负责装载Classpath路径下的类包。

JVM装载类时使用“双亲委托机制”,“双亲委托”是指当一个ClassLoader装载一个类时,除非显式地使用另一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入:“委托机制”是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。 ClassLoader的重要方法: Class loadClass(String name):name参数指定类装载器需要装载类的名字,必须使用全限定类名。该方法有一个重载方法loadClass(String name,boolean resolve),resolve参数告诉类装载器是否需要解析该类。在初始化类之前,应考虑进行类解析的工作,但并不是所有的类都需要解析,若JVM值需知道该类是否存在或找出该类的超类,那么就不需要进行解析。 Class defineClass(String name,byte[] b,int off,int len):将类文件的字节数组转换成JVM内部的java.lang.Class对象。字节数组可以从本地文件系统、远程网络获取。name为字节数组对应的全限定类名。 Class findSystemClass(String name):从本地文件系统载入Class文件,若本地文件系统更不存在该Class文件,将抛出ClassNotFoundException异常。 Class findLoadedClass():调用该方法来查看ClassLoader是否已装入某个类。若已装入,则返回java.lang.Class对象,否则返回null。 ClassLoader getParent():获取类装载器的父装载器。

参考:www.cnblogs.com/fengbs/p/70…

17,常见的限流算法有:令牌桶、漏桶、计数器。

1.令牌桶限流 令牌桶是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌,填满了就丢弃令牌,请求是否被处理要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求。令牌桶允许一定程度突发流量,只要有令牌就可以处理,支持一次拿多个令牌。令牌桶中装的是令牌。

2.漏桶限流(基于队列容器指定空间) 漏桶一个固定容量的漏桶,按照固定常量速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。漏桶可以看做是一个具有固定容量、固定流出速率的队列,漏桶限制的是请求的流出速率。漏桶中装的是请求。

3.计数器限流(hystrix是改良版的时间窗口计数,把一个时间段分为多个时间段计数,更加平滑均匀) 有时我们还会使用计数器来进行限流,主要用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。

漏桶和令牌桶的比较 令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。

整体而言,令牌桶算法更优(可动态调整),令牌桶算法能平滑流量,但是实现更为复杂一些。

1、计数器算法

采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过 AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”。

2、漏桶算法(基于队列容器指定空间) 为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

3、令牌桶算法 从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。(Guava RateLimiter提供了令牌桶算法实现)

4、集群限流 前面讨论的几种算法都属于单机限流的范畴,但是业务需求五花八门,简单的单机限流,根本无法满足他们。 比如为了限制某个资源被每个用户或者商户的访问次数,5s只能访问2次,或者一天只能调用1000次,这种需求,单机限流是无法实现的,这时就需要通过集群限流进行实现。如何实现?为了控制访问次数,肯定需要一个计数器,而且这个计数器只能保存在第三方服务,比如redis。大概思路:每次有相关操作的时候,就向redis服务器发送一个incr命令,比如需要限制某个用户访问/index接口的次数,只需要拼接用户id和接口名生成redis的key,每次该用户访问此接口时,只需要对这个key执行incr命令,在这个key带上过期时间,就可以实现指定时间的访问频率。

参考:blog.csdn.net/qq_35642036… blog.csdn.net/p312011150/…

转载于:https://juejin.im/post/5cac12b1f265da038260f556

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值