一、分布式与微服务(46)

1、什么是CAP理论

CAP理论是分布式领域中非常重要的一个指导理论,C(Consistency)表示强一致性,A(Availability)表示可用性,P(Partition Tolerance)表示分区容错性,CAP理论指出在目前的硬件条件下,一个分布式系统是必须要保证分区容错性的,而在这个前提下,分布式系统要么保证CP,要么保证AP,无法同时保证CAP。
分区容错性表示,一个系统虽然是分布式的,但是对外看上去应该是一个整体,不能由于分布式系统内部的某个结点挂点,或网络出现了故障,而导致系统对外出现异常。所以,对于分布式系统而⾔是一定要保证分区容错性的。
强一致性表示,一个分布式系统中各个结点之间能及时的同步数据,在数据同步过程中,是不能对外提 供服务的,不然就会造成数据不一致,所以强一致性和可用性是不能同时满足的。
可用性表示,一个分布式系统对外要保证可用。

2、什么是BASE理论

由于不能同时满足CAP,所以出现了BASE理论:

  1. BA:Basically Available,表示基本可用 ,表示可以允许一定程度的不可用 ,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的
  2. S:Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步
  3. E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的

    3、什么是RPC

    RPC,表示远程过程调用,对于Java这种面试对象语⾔,也可以理解为远程方法调用 ,RPC调用和 HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP 协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的。

    4、数据一致性模型有哪些

  4. 强一致性:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值,这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP理论,这种实现需要牺牲可用性。

  5. 弱一致性:系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。用户读到某一操作对系统数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。
  6. 最终一致性:最终一致性是弱一致性的特例,强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。到达最终一致性的时间,就是不一致窗口时间,在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。最终一致性模型根据其提供的不同保证可以划分为更多的模型,包括因果一致性和会话一致性等。

    5、分布式ID是什么?有哪些解决方案?

    在开发中,我们通常会需要一个唯一ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护一个自增数字来作为ID都是可以的,但对于一个分布式系统,就会有可能会出现ID冲突,此时有以下解决方案:

  7. uuid,这种方案复杂度最低,但是会影响存储空间和性能

  8. 利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案
  9. 利用Redis 、zookeeper的特性来生成id,比如Redis的自增命令 、zookeeper的顺序节点,这种方案和单机数据库(mysql)相比,性能有所提高,可以适当选用
  10. 雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增 。业界存在tinyid 、leaf等开源中间件实现了雪花算法。

    6、分布式锁的使用场景是什么?有哪些实现方案?

    在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用 ReentrantLock 、synchronized等技术来作为锁,来控制共享资源的使用。
    而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用 ReentrantLock 、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。

目前主流的分布式锁的实现方案有两种:

  1. zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱
  2. Redis:利用Redis的setnx、lua脚本、消费订阅等机制来实现的,Redis分布式锁的特点是高可用,因为Redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定 ( 一旦Redis中的数据出现了不一致) ,可能会出现多个客户端同时加到锁的情况

    7、什么是分布式事务?有哪些实现方案?

    在分布式系统中,一次业务处理可能需要多个应用来实现,比如用户发送一次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于一次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。
    常用解决方案有:

  3. 本地消息表:创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台 定时任务从本地消息表中取出未成功的消息,重试调用库存系统

  4. 消息队列:目前RocketMQ中支持事务消息,它的工作原理是:
    1. 生产者订单系统先发送一条half消息到Broker,half消息对消费者而⾔是不可见的
    2. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
    3. 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
    4. 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
    5. 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理
  5. Seata:阿里开源的分布式事务框架,支持AT 、TCC等多种模式,底层都是基于两阶段提交理论来实现的

    8、什么是ZAB协议

    ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性 的,分为三个阶段:

  6. 领导者选举阶段:从Zookeeper集群中选出一个节点作为Leader,所有的写请求都会由Leader节点来处理

  7. 数据同步阶段:集群中所有节点中的数据要和Leader节点保持一致,如果不一致则要进行同步
  8. 请求广播阶段:当Leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使得写请求像事务一样在其他节点上执行,达到节点上的数据实时一致

但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。

9、简述paxos算法

Paxos算法解决的是一个分布式系统如何就某个值 ( 决议) 达成一致。一个典型的场景是,在一个分布 式数据库系统中,如果各个节点的初始状态一致,每个节点执行相同的操作序列,那么他们最后能够得到一个一致的状态。为了保证每个节点执行相同的操作序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致 。在Paxos算法中,有三种⻆色:Proposer (提议者)、Acceptor (接受者) 、Learners (记录员)

  1. Proposer提议者:只要Proposer发的提案Propose被半数以上的Acceptor接受,Proposer就认为该提案例的value被选定了。
  2. Acceptor接受者:只要Acceptor接受了某个提案,Acceptor就认为该提案例的value被选定了
  3. Learner记录员:Acceptor告诉Learner哪个value被选定,Learner就认为哪个value被选定 。

Paxos算法分为两个阶段,具体如下:
阶段一(preprae):
(a) Proposer收到client请求或者发现本地有未提交的值,选择一个提案编号N ,然后向半数以上的Acceptor发送编号为 N的Prepare请求。
(b) Acceptor收到一个编号为N的Prepare请求,如果该轮paxos本节点已经有已提交的value记录,对比记录的编号和N,大于N则拒绝回应,否则返回该记录 value及编号没有已提交记录,判断本地是否有编号N1,N1>N 、则拒绝响应,否则将N1改为N ( 如果没有N1,则记录N) ,并响应prepare
阶段二(accept)
(a) 如果 Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对[N,V]提案的 Accept请求给半数以上的Acceptor。V就是收到的响应中编号最大的value,如果响应中不包含任何value,那么V 就由 Proposer 自已决定 。
(b) 如果 Acceptor收到一个针对编号为N的提案的 Accept请求 ,Acceptor对比本地的记录编号 ,如果小于等于N ,则接受该值,并提交记录value 。否则拒绝请求 Proposer

如果收到的大多数Acceptor响应,则选定该value值,并同步给leaner,使未响应的Acceptor 达成一致
活锁:accept时被拒绝,加大N,重新accept,此时另外一个proposer也进行相同操作,导致accept一 致失败,无法完成算法
multi-paxos:区别于paxos只是确定一个值,multi-paxos可以确定多个值,收到accept请求后,则一 定时间内不再accept其他节点的请求,以此保证后续的编号不需要在经过preprae确认,直接进行 accept操作。此时该节点成为了leader,直到accept被拒绝,重新发起prepare请求竞争leader资格。

10、简述raft算法

概念:
分布式一致性算法:raft会先选举出leader,leader完全负责replicatedlog的管理。leader负责接受所有 客户端更新请求,然后复制到follower节点,并在“安全”的时候执行这些请求 。如果leader 故障,followes会重新选举出新的leader
三种状态:一个节点任一时刻处于三者之一
leader:处理所有的客户端请求 ( 如果客户端将请求发给了Follower,Follower将请求重定 向给 Leader)
follower:不会发送任何请求,只会简单地响应来自Leader或Candidate的请求
candidate:用于选举产生新的leader(候选人)
term:任期,leader产生到重新选举为一任期,每个节点都维持着当前的任期号
term是递增的,存储在log日志的entry中,代表当前entry是在哪一个term时期写入 每个任期只能有一 个leader或者没有(选举失败)
每次rpc通信时传递该任期号,如果RPC收到任期号大于本地的 、切换为follower,小于本地任期号则返回错误信息
两个RPC通信:
RequestVote RPC:负责选举,包含参数lastIndex,lastTerm AppendEntries RPC:负责数据的交 互。
日志序列:每一个节点上维持着一份持久化Log,通过一致性协议算法,保证每一个节点中的Log 保持 一致,并且顺序存放,这样客户端就可以在每一个节点中读取到相同的数据

状态机: 日志序列同步到多数节点时,leader将该日志提交到状态机,并在下一次心跳通知所有节点提 交状态机 ( 携带最后提交的lastIndex)

何时触发选举:
集群初始化时,都是follower,随机超时,变成candidate,发起选举
如果follower在election timeout内没有收到来自leader的心跳,则主动触发选举
选举过程:发出选举的节点⻆度
1、增加节点本地的term,切换到candidate状态
2、投自已一票
其他节点投票逻辑:每个节点同一任期最多只能投一票,候选人知道的信息不能比自已少 ( 通过副本 日志和安全机制保障) ,先来先得
3、并行给其他节点发送RequestVote RPCs(选举请求) 、包含term参数
4、等待回复
4.1、收到majority(大多数)的投票,赢得选举,切换到leader状态,立刻给所有节点发心跳消息
4.2、被告知别人当选,切换到follower状态。 ( 原来的leader对比term,比自已的大,转换到
follower状态)
4.3 、一段时间没收到majority和leader的心跳通知,则保持candidate 、重新发出选举

日志序列同步: 日志需要存储在磁盘持久化,崩溃可以从日志恢复
1、客户端发送命令给Leader。
2、Leader把日志条目加到自已的日志序列里。
3、Leader发送AppendEntriesRPC请求给所有的follower 。携带了prevLogIndex,prevLogTerm follower收到后,进行日志序列匹配
匹配上则追加到自已的日志序列
匹配不上则拒绝请求,leader将日志index调小,重新同步直至匹配上 ,follower将leader的日志 序列覆盖到本地

一旦新的日志序列条目变成majority的了,将日志序列应用到状态机中
Leader在状态机里提交自已日志序列条目,然后返回结果给客户端
Leader下次发送AppendEntriesRPC时,告知follower已经提交的日志序列条目信息(lastIndex) follower收到RPC后,提交到自已的状态机里
提交状态机时,如果term为上一任期,必须与当前任期数据一起提交,否则可能出现覆盖已提交状态机的日志

新选举出的leader一定拥有所有已提交状态机的日志条目
leader在当日志序列条目已经复制到大多数follower机器上时,才会提交日志条目 。

而选出的leader的logIndex必须大于等于大多数节点,因此leader肯定有最新的日志

安全原则:
选举安全原则:对于一个给定的任期号,最多只会有一个领导人被选举出来
状态机安全原则:如果一个leader已经在给定的索引值位置的日志条目应用到状态机中,那么其他任何 的服务器在这个索引位置不会提交一个不同的日志
领导人完全原则:如果某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任 期 号的所有领导人中
领导人只附加原则:领导人绝对不会删除或者覆盖自已的日志,只会增加
日志匹配原则:如果两个日志在相同的索引位置的日志条目的任期号相同,那么我们就认为这个日 志从头到这个索引位置之间全部完全相同

11、为什么Zookeeper可以用来作为注册中心

可以利用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的 数据都是存在内存中的,并且Zookeeper底层采用了nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么Zookeeper 则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用 ,所以用Redis 、Eureka 、Nacos来作为注册中心将更合适。

12、Zookeeper中的领导者选举的流程是怎样的?

对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:

  1. 集群中各个节点首先都是观望状态(LOOKING),一开始都会投票给自已,认为自已比较适合作为leader
  2. 然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid ,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜
  3. 一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自已的投票箱中,并将新的选票发送给其他节点
  4. 如果pk是平局则将接收到的选票放入自已的投票箱中
  5. 如果pk赢了,则忽略所接收到的选票
  6. 当然一个节点将一张选票放入到自已的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自已所投的节点是一样的,如果超过半数,那么则认为当前自已所投的节点是leader
  7. 集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了

    13、Zookeeper集群中节点之间数据是如何同步的

  8. 首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer

  9. 然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式
  10. 集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求
  11. Leader节点收到一个写请求时,会通过两阶段机制来处理
  12. Leader节点会将该写请求对应的日志发送给其他Follower节点,并等待Follower节点持久化日志成功
  13. Follower节点收到日志后会进行持久化,如果持久化成功则发送一个Ack给Leader节点
  14. 当Leader节点收到半数以上的Ack后,就会开始提交,先更新Leader节点本地的内存数据
  15. 然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据
  16. 同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执行更新本地内存数据
  17. 最后Leader节点返回客户端写请求响应成功
  18. 通过同步机制和两阶段提交机制来达到集群中节点数据一致

    14、Dubbo支持哪些负载均衡策略

  19. 随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率

  20. 轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
  21. 最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
  22. 一致性哈希:相同参数的请求总是发到同一个服务提供者

    15、Dubbo是如何完成服务导出的?

  23. 首先Dubbo会将程序员所使用的@DubboService注解或@Service注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。

  24. 然后调用ServiceBean的export方法进行服务导出
  25. 然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
  26. 将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
  27. 还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat 、Netty等

    16、Dubbo是如何完成服务引入的?

  28. 当程序员使用@Reference注解来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么

  29. 然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
  30. 并绑定一些监听器用来监听动态配置中心的变更
  31. 然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean

    17、Dubbo的架构设计是怎样的?

    Dubbo中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:

  32. Proxy服务代理层,支持JDK动态代理 、javassist等代理机制

  33. Registry注册中心层,支持Zookeeper 、Redis等作为注册中心
  34. Protocol远程调用层,支持Dubbo 、Http等调用协议
  35. Transport网络传输层,支持netty 、mina等网络传输框架
  36. Serialize数据序列化层,支持JSON 、Hessian等序列化机制

各层说明

  1. config配置层:对外配置接口,以ServiceConfig , ReferenceConfig为中心,可以直接初始化配置类,也可以通过spring 解析配置生成配置类
  2. proxy 服务代理层:服务接口透明代理,生成服务的客户端Stub 和服务器端 Skeleton, 以ServiceProxy 为中心,扩展接口为 ProxyFactory
  3. registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory , Registry , RegistryService
  4. cluster 路由层: 封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster , Directory , Router , LoadBalance
  5. monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory , Monitor , MonitorService
  6. protocol 远程调用层:封装 RPC 调用,以 Invocation, Result为中心,扩展接口为Protocol , Invoker , Exporter
  7. exchange 信息交换层:封装请求响应模式,同步转异步,以Request , Response为中心,扩展接口为 Exchanger , ExchangeChannel , ExchangeClient , ExchangeServer
  8. transport网络传输层:抽象 mina 和 netty 为统一接口 ,以 Message 为中心,扩展接口为Channel
  9. serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization , ObjectInput , ObjectOutput , ThreadPool

关系说明

  1. 在RPC 中,Protocol 是核心层,也就是只要有Protocol + Invoker + Exporter 就可以完成非透明 的RPC 调用,然后在Invoker 的主过程上 Filter 拦截点。
  2. 图中的Consumer 和Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用Client 和Server 的原因是Dubbo 在很多场景下都使用Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。
  3. 而Cluster 是外围概念,所以Cluster 的目的是将多个Invoker 伪装成一个 Invoker,这样其它人只要关注Protocol 层Invoker 即可,加上Cluster 或者去掉Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。
  4. Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使 用时,才用 Proxy 将 Invoker 转成接口 ,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
  5. 而Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上 ,Remoting 内部再划为Transport 传输层和Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展UDP 传输,而Exchange 层是在传输层 之上封装了Request-Response 语义。
  6. Registry 和Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起

image.png

18、负载均衡算法有哪些

  1. 轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服 务器实际的连接数和当前的系统负载。
  2. 随机法:通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
  3. 源地址哈希法:源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址 哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端 服务器进行访问。
  4. 加权轮询法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
  5. 加权随机法:与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
  6. 最小连接数法:最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

    19、分布式架构下,Session 共享有什么方案

  7. 采用无状态服务,抛弃Session

  8. 存入Cookie ( 有安全风险)
  9. 服务器之间进行 Session 同步,这样可以保证每个服务器上都有全部的 Session 信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
  10. IP绑定策略
  11. 使用Nginx(或其他复杂均衡软硬件)中的 IP 绑定策略,同一个 IP 只能在指定的同一个机器访问,但 是这样做失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大;
  12. 使用Redis 存储:把Session 放到Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis,但是这种方案带来的好处也是很大的:
    1. 实现了Session 共享;
    2. 可以水平扩展(增加 Redis 服务器)
    3. 服务器重启Session 不丢失 ( 不过也要注意 Session 在Redis 中的刷新/失效机制) ;
    4. 不仅可以跨服务器Session 共享,甚至可以跨平台(例如网页端和 APP 端)

      20、简述你对RPC、RMI的理解

      RPC:在本地调用远程的函数,远程过程调用 ,可以跨语⾔实现 httpClient
      RMI:远程方法调用,java中用于实现RPC的一种机制,RPC的java版本,是J2EE的网络调用机制,跨 JVM调用对象的方法,面向对象的思维方式

直接或间接实现接口 java.rmi.Remote 成为存在于服务器端的远程对象,供客户端访问并提供一定的服务

远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理 ,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。

  1. public interface IService extends Remote {
  2. String service(String content) throws RemoteException;
  3. }
  4. public class ServiceImpl extends UnicastRemoteObject implements IService {
  5. private String name;
  6. public ServiceImpl(String name) throws RemoteException {
  7. this.name = name;
  8. }
  9. @Override
  10. public String service(String content) {
  11. return "server >> " + content;
  12. }
  13. }
  14. public class Server {
  15. public static void main(String[] args) {
  16. try {
  17. IService service02 = new ServiceImpl("service02");
  18. Context namingContext = new InitialContext();
  19. namingContext.rebind("rmi://127.0.0.1/service02", service02);
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. System.out.println("000000!");
  24. }
  25. }
  26. public class Client {
  27. public static void main(String[] args) {
  28. String url = "rmi://127.0.0.1/";
  29. try {
  30. Context namingContext = new InitialContext();
  31. IService service02 = (IService) namingContext.lookup(url + "service02");
  32. Class stubClass = service02.getClass();
  33. System.out.println(service02 + " is " + stubClass.getName()); //com.sun.proxy.$Proxy0
  34. Class[] interfaces = stubClass.getInterfaces();
  35. for (Class c : interfaces) {
  36. System.out.println("implement" + c.getName() + " interface");
  37. }System.out.println(service02.service("hello"));
  38. } catch (Exception e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }

21、如何实现接口的幂等性

  1. 唯一id 。每次操作,都根据操作和内容生成唯一的id,在执行之前先判断id是否存在,如果不存在则执行后续操作,并且保存到数据库或者Redis等。
  2. 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token携带过去,务器判断token是否存在Redis中,存在表示第一次请求,可以继续执行业务,执行业务完成后,最后需要把Redis中的token删除
  3. 建去重表。将业务中有唯一标识的字段保存到去重表,如果表中存在,则表示已经处理过了
  4. 版本控制 。增加版本号,当版本号符合时,才能更新数据
  5. 状态控制。例如订单有状态已支付、未支付、支付中、支付失败,当处于未支付的时候才允许修改为支付中等

    22、Zookeeper的数据模型和节点类型

    数据模型:树形结构

zk维护的数据主要有:客户端的会话 ( Session) 状态及数据节点 ( dataNode) 信息。

zk在内存中构造了个DataTree的数据结构,维护着path到dataNode的映射以及dataNode间的树状层级 关系。为了提高读取性能,集群中每个服务节点都是将数据全量存储在内存中。所以,zk最适于读多写 少且轻量级数据的应用场景。

数据仅存储在内存是很不安全的,zk采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢 失的情况下能快速恢复。

树中的每个节点被称为— Znode

Znode 兼具文件和目录两种特点。可以做路径标识,也可以存储数据,并可以具有子 Znode 。具有 增、删、改、查等操作。

Znode 具有原子性操作,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。 另外,每一个节点都拥有自已的ACL(访问控制列 表),这个列表规定了用户的权限,即限定了特定用户 对目标节点可以执行的操作

Znode 存储数据大小有限制。每个Znode 的数据大小至多 1M,常规使用中应该远小于此值。

Znode 通过路径引用,如同 Unix 中的文件路径 。路径必须是绝对的,因此他们必须由斜杠字符来开 头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在 ZooKeeper 中,路径由Unicode 字符串组成,并且有一些限制。字符串”/zookeeper”用以保存管理信 息,比如关键配额信息。

持久节点:一旦创建、该数据节点会一直存储在zk服务器上、即使创建该节点的客户端与服务端的会话 关闭了、该节点也不会被删除

临时节点:当创建该节点的客户端会话因超时或发生异常而关闭时、该节点也相应的在zk上被删除。

有序节点:不是一种单独种类的节点、而是在持久节点和临时节点的基础上、增加了一个节点有序的性质

23、简述zk的命名服务、配置管理、集群管理

命名服务:
通过指定的名字来获取资源或者服务地址。Zookeeper可以创建一个全局唯一的路径,这个路径就可以 作为一个名字。被命名的实体可以是集群中的机器,服务的地址,或者是远程的对象等。一些分布式服务框架 ( RPC 、RMI) 中的服务地址列表,通过使用命名服务,客户端应用能够根据特定的名字来获取资源的实体、服务地址和提供者信息等

配置管理:
实际项目开发中,经常使用 .properties或者xml需要配置很多信息,如数据库连接信息 、fps地址端口等 等。程序分布式部署时,如果把程序的这些配置信息保存在zk的znode节点下,当你要修改配置,即 znode会发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。

集群管理:
集群管理包括集群监控和集群控制,就是监控集群机器状态,剔除机器和加入机器。zookeeper可以方 便集群机器的管理,它可以实时监控znode节点的变化,一旦发现有机器挂了,该机器就会与zk断开连接,对应的临时目录节点会被删除,其他所有机器都收到通知。新机器加入也是类似。

24、讲下Zookeeper中的watch机制

客户端,可以通过在znode上设置watch,实现实时监听znode的变化

Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端
● 父节点的创建,修改,删除都会触发Watcher事件。
● 子节点的创建,删除会触发Watcher事件。

一次性:一旦被触发就会移除,再次使用需要重新注册,因为每次变动都需要通知所有客户端,一次性可以减轻压力,3.6.0默认持久递归,可以触发多次

轻量:只通知发生了事件,不会告知事件内容,减轻服务器和带宽压力

Watcher 机制包括三个⻆色:客户端线程 、客户端的 WatchManager 以及 ZooKeeper 服务器

  1. 客户端向ZooKeeper 服务器注册一个Watcher 监听,
    2. 把这个监听信息存储到客户端的WatchManager 中
    3. 当ZooKeeper 中的节点发生变化时,会通知客户端,客户端会调用相应 Watcher 对象中的回调 方法。watch回调是串行同步的

25、Zookeeper和Eureka的区别

zk:CP设计(强一致性),目标是一个分布式的协调系统,用于进行资源的统一管理。
当节点crash后,需要进行leader的选举,在这个期间内 ,zk服务是不可用的。

eureka:AP设计 ( 高可用) ,目标是一个服务注册发现系统,专⻔用于微服务的服务发现注册。
Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时如果发现连接失败,会自动切换至其他节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)

同时当eureka的服务端发现85%以上的服务都没有心跳的话,它就会认为自已的网络出了问题,就不会从服务列表中删除这些失去心跳的服务,同时eureka的客户端也会缓存服务信息。eureka对于服务注册 发现来说是非常好的选择。

26、如何实现分库分表

将原本存储于单个数据库上的数据拆分到多个数据库,把原来存储在单张数据表的数据拆分到多张数据 表中,实现数据切分,从而提升数据库操作性能。分库分表的实现可以分为两种方式:垂直切分和水平切分。

  1. 水平:将数据分散到多张表,涉及分区键,
  2. 分库:每个库结构一样,数据不一样,没有交集。库多了可以缓解io和cpu压力
  3. 分表:每个表结构一样,数据不一样,没有交集。表数量减少可以提高sql执行效率 、减轻cpu压力
  4. 垂直:将字段拆分为多张表,需要一定的重构
  5. 分库:每个库结构、数据都不一样,所有库的并集为全量数据
  6. 分表:每个表结构、数据不一样,至少有一列交集,用于关联数据,所有表的并集为全量数据

    27、存储拆分后如何解决唯一主键问题

  7. UUID:简单 、性能好,没有顺序,没有业务含义,存在泄漏mac地址的风险

  8. 数据库主键:实现简单,单调递增,具有一定的业务可读性,强依赖db 、存在性能瓶颈,存在暴露业务信息的风险
  9. Redis、mongodb、zk等中间件:增加了系统的复杂度和稳定性
  10. 雪花算法

    28、雪花算法原理

    image.png
    第一位符号位固定为0 ,41位时间戳,10位workId,12位序列号,位数可以有不同实现。
    优点:每个毫秒值包含的ID值很多,不够可以变动位数来增加,性能佳 ( 依赖workId的实现) 。时间戳值在高位,中间是固定的机器码,自增的序列在低位,整个ID是趋势递增的 。能够根据业务场景数据库节点布置灵活调整bit位划分,灵活度高。
    缺点:强依赖于机器时钟,如果时钟回拨,会导致重复的ID生成,所以一般基于此的算法发现时钟回拨,都会抛异常处理,阻止ID生成,这可能导致服务不可用 。

    29、如何解决不使用分区键的查询问题

  11. 映射:将查询条件的字段与分区键进行映射,建一张单独的表维护(使用覆盖索引)或者在缓存中维护

  12. 基因法:分区键的后x个bit位由查询字段进行hash后占用,分区键直接取x个bit位获取分区,查询字段进行hash获取分区,适合非分区键查询字段只有一个的情况
  13. 冗余:查询字段冗余存储

    30、Spring Cloud有哪些常用组件,作用是什么?

  14. Eureka:注册中心

  15. Nacos:注册中心、配置中心
  16. Consul:注册中心、配置中心
  17. Spring Cloud Config:配置中心
  18. Feign/OpenFeign:RPC调用
  19. Kong:服务网关
  20. Zuul:服务网关
  21. Spring Cloud Gateway:服务网关
  22. Ribbon:负载均衡
  23. Spring CLoud Sleuth:链路追踪
  24. Zipkin:链路追踪
  25. Seata:分布式事务
  26. Dubbo:RPC调用
  27. Sentinel:服务熔断
  28. Hystrix:服务熔断

    31、如何避免缓存穿透、缓存击穿、缓存雪崩?

    缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
    解决方案:

  29. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

  30. 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
  31. 缓存预热互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap 中,一个一定不存在的数据会被这个bitmap 拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同
时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:

  1. 设置热点数据永远不过期。加互斥锁

    32、分布式系统中常用的缓存方案有哪些

  2. 客户端缓存:页面和浏览器缓存,APP缓存,H5缓存,localStorage 和 SessionStorage CDN缓存:内容存储:数据的缓存,内容分发:负载均衡

  3. nginx缓存:静态资源
  4. 服务端缓存:本地缓存,外部缓存
  5. 数据库缓存:持久层缓存 ( mybatis,hibernate多级缓存) ,mysql查询缓存
  6. 操作系统缓存: PageCache 、BufferCache

    33、缓存过期都有哪些策略?

  7. 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量

  8. 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除 。该策略可以最大化地节省CPU资源,但是很消耗内存、许多的过期数据都还存在内存中。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  9. 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key(是随机的),并清除其中已过期的key 。该策略是定时过期和惰性过期的折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
  10. 分桶策略:定期过期的优化,将过期时间点相近的key放在一起,按时间扫描分桶。

    34、常见的缓存淘汰算法

  11. FIFO(First In First Out,先进先出),根据缓存被存储的时间,离当前最远的数据优先被淘汰;

  12. LRU(LeastRecentlyUsed,最近最少使用),根据最近被使用的时间,离当前最远的数据优先被淘汰
  13. LFU(LeastFrequentlyUsed,最不经常使用),在一段时间内,缓存数据被使用次数最少的会被淘汰。

    35、布隆过滤器原理,优缺点

  14. 位图:int[10],每个int类型的整数是4*8=32个bit,则int[10]一共有320 bit,每个bit非0即1,初始化时都是0

  15. 添加数据时:将数据进行hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个,则 一个数据添加会将多个 ( hash函数个数) bit改为1,多个hash函数的目的是减少hash碰撞的概率
  16. 查询数据:hash函数计算得到hash值,对应到bit中,如果有一个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中


优点:

  1. 占用内存小
  2. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势数据量很大时,布隆过滤器可以表示全集
  4. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

缺点:

  1. 误判率,即存在假阳性(False Position),不能准确判断元素是否在集合中不能获取元素本身
  2. 一般情况下不能从布隆过滤器中删除元素

    36、分布式缓存寻址算法

  3. hash算法:根据key进行hash函数运算、结果对分片数取模,确定分片适合固定分片数的场景,扩展分片或者减少分片时,所有数据都需要重新计算分片、存储

  4. 一致性hash:将整个hash值得区间组织成一个闭合的圆环,计算每台服务器的hash值、映射到圆环中 。使用相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,找到的第一个服务器就 是数据存储的服务器。新增及减少节点时只会影响节点到他逆时针最近的一个服务器之间的值存在 hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
  5. hash slot:将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进行hash决定存放的slot,新增及删除节点时,将slot进行迁移即可

    37、什么是Hystrix?简述实现机制

    分布式容错框架

  6. 阻止故障的连锁反应,实现熔断

  7. 快速失败,实现优雅降级
  8. 提供实时的监控和告警

资源隔离:线程隔离,信号量隔离

  1. 线程隔离:Hystrix会给每一个Command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其他线程池造成影响
  2. 信号量隔离:客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。

熔断和降级:调用服务失败后快速失败

熔断是为了防止异常不扩散,保证系统的稳定性

降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报错,只是服务水平下降

  1. 通过HystrixCommand 或者HystrixObservableCommand 将所有的外部系统 ( 或者称为依赖) 包装起来,整个包装对象是单独运行在一个线程之中 ( 这是典型的命令模式) 。
  2. 超时请求应该超过你定义的阈值
  3. 为每个依赖关系维护一个小的线程池 ( 或信号量) ; 如果它变满了,那么依赖关系的请求将立即被拒绝,而不是排队等待。
  4. 统计成功,失败 (由客户端抛出的异常) ,超时和线程拒绝。
  5. 打开断路器可以在一段时间内停止对特定服务的所有请求,如果服务的错误百分比通过阈值,手动或自动的关闭断路器。
  6. 当请求被拒绝、连接超时或者断路器打开,直接执行fallback逻辑。
  7. 近乎实时监控指标和配置变化。

    38、Spring Cloud和Dubbo有哪些区别?

    Spring Cloud是一个微服务框架,提供了微服务领域中的很多功能组件,Dubbo一开始是一个RPC调用 框架,核心是解决服务调用间的问题,Spring Cloud是一个大而全的框架,Dubbo则更侧重于服务调用,所以Dubbo所提供的功能没有Spring Cloud全面,但是Dubbo的服务调用性能比Spring Cloud高,不过Spring Cloud和Dubbo并不是对立的,是可以结合起来一起使用的。

    39、什么是服务雪崩?什么是服务限流?

  8. 当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用 ,这就 是服务雪崩,解决方式就是服务降级和服务熔断。

  9. 服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。

    40、什么是服务熔断?什么是服务降级? 区别是什么?

  10. 服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自已不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力 ,直到服务B恢复。

  11. 服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压力,这就是服务降级。

相同点:

  1. 都是为了防止系统崩溃
  2. 都让用户体验到某些功能暂时不可用

不同点:熔断是下游服务故障触发的,降级是为了降低系统负载

41、SOA、分布式、微服务之间有什么关系和区别?

  1. 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基本上都是分布式架构的
  2. SOA是一种面向服务的架构,系统的所有服务都注册在总线上,当调用服务时,从总线上查找服务信息,然后调用
  3. 微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应的一个服务的架构

    42、怎么拆分微服务?

    拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:

  4. 微服务之间尽量不要有业务交叉。

  5. 微服务之前只能通过接口进行服务调用,而不能绕过接口直接访问对方的数据。
  6. 高内聚,低耦合。

    43、怎样设计出高内聚、低耦合的微服务?

    高内聚低耦合,是一种从上而下指导微服务设计的方法。实现高内聚低耦合的工具主要有同步的接口调 用 和异步的事件驱动两种方式。

    44、有没有了解过DDD领域驱动设计?

    什么是DDD: 在2004年,由Eric Evans提出了,DDD是面对软件复杂之道 。Domain-Driven- Design –Tackling Complexity in the Heart of Software
    大泥团: 不利于微服务的拆分。大泥团结构拆分出来的微服务依然是泥团机构,当服务业务逐渐复杂,这个泥团又会膨胀成为大泥团。
    DDD只是一种方法论,没有一个稳定的技术框架。DDD要求领域是跟技术无关、跟存储无关、跟通信无关。

    45、什么是中台?

    所谓中台,就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用 的组件。
    大体上,中台可以分为三类业务中台、数据中台和技术中台。大数据杀熟-数据中台
    中台跟DDD结合: DDD会通过限界上下文将系统拆分成一个一个的领域,而这种限界上下文,天生就 成了中台之间的逻辑屏障。
    DDD在技术与资源调度方面都能够给中台建设提供不错的指导。
    DDD分为战略设计和战术设计。上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。

    46、你的项目中是怎么保证微服务敏捷开发的?

  7. 开发运维一体化。

  8. 敏捷开发:目的就是为了提高团队的交付效率,快速迭代,快速试错
  9. 每个月固定发布新版本,以分支的形式保存到代码仓库中。快速入职。任务面板、站立会议。团队 人员灵活流动,同时形成各个专家代表
  10. 测试环境- 生产环境 -开发测试环境SIT-集成测试环境-压测环境STR-预投产环境-生产环境PRD
  11. 晨会、周会、需求拆分会

    二、消息队列(28)

    1、如何进行产品选型?

  12. Kafka:

    1. 优点: 吞吐量非常大,性能非常好,集群高可用。
    2. 缺点:会丢数据,功能比较单一。
    3. 使用场景: 日志分析、大数据采集
  13. RabbitMQ:
    1. 优点: 消息可靠性高,功能全面。
    2. 缺点:吞吐量比较低,消息积累会严重影响性能。erlang语⾔不好定制。
    3. 使用场景:小规模场景。
  14. RocketMQ:

    1. 优点:高吞吐、高性能、高可用,功能非常全面。
    2. 缺点:开源版功能不如云上商业版。官方文档和周边生态还不够成熟。客户端只支持java。
    3. 使用场景:几乎是全场景。

      2、简述RabbitMQ的架构设计

      Broker:rabbitmq的服务节点
      Queue:队列,是RabbitMQ的内部对象,用于存储消息 。RabbitMQ中消息只能存储在队列中 。生产者投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(轮询)给多个消费者进行消费,而不是每个消费者都收到所有的消息进行消费 。(注意:RabbitMQ不支持队列层面的广播消费,如果需要广播消费,可以采用一个交换器通过路由Key绑定多个队列,由多个消费者来订阅这些队列的方式。
      Exchange:交换器。生产者将消息发送到Exchange,由交换器将消息路由到一个或多个队列中 。如果路由不到,或返回给生产者,或直接丢弃,或做其它处理。
      RoutingKey:路由Key 。生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则。这个路由Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键固定的情况下,生产者可以在发送消息给交换器时通过指定RoutingKey来决定消息流向哪里。
      Binding:通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就可以指定如何正确的路由到队列了。
      交换器和队列实际上是多对多关系。就像关系数据库中的两张表。他们通过BindingKey做关联(多对多关系表) 。在投递消息时,可以通过Exchange和RoutingKey(对应BindingKey)就可以找到相对应的队列。
      信道:信道是建立在Connection 之上的虚拟连接。当应用程序与Rabbit Broker建立TCP连接的时候,客户端紧接着可以创建一个AMQP 信道(Channel) ,每个信道都会被指派一个唯一的D 。RabbitMQ 处 理的每条AMQP 指令都是通过信道完成的。信道就像电缆里的光纤束。一条电缆内含有许多光纤束,允许所有的连接通过多条光线束进行传输和接收。

      3、RabbitMQ如何确保消息发送? 消息接收?

      发送方确认机制:信道需要设置为confirm 模式,则所有在信道上发布的消息都会分配一个唯一ID 。 一旦消息被投递到queue ( 可持久化的消息需要写入磁盘) ,信道会发送一个确认给生产者 ( 包含消息 唯一ID) 。如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条nack ( 未确认) 消息给生产 者 。所有被发送的消息都将被confirm ( 即 ack) 或者被nack一次。但是没有对消息被confirm 的快 慢做任何保证,并且同一条消息不会既被confirm又被nack 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调方法会被触发。
      ConfirmCallback接口:只确认是否正确到达 Exchange 中,成功到达则回调
      ReturnCallback接口:消息失败返回时回调
      接收方确认机制:消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(或者磁盘,持久化消息)中移去消息 。否则,消息被消费后会被立即删除。
      消费者接收每一条消息后都必须进行确认 ( 消息接收和消息确认是两个不同操作) 。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
      RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可 以很长。保证数据的最终一致性;
      如果消费者返回ack之前断开了链接,RabbitMQ 会重新分发给下一个订阅的消费者。( 可能存在消息重复消费的隐患,需要去重)

      4、RabbitMQ事务消息

      通过对信道的设置实现
  15. channel.txSelect(); 通知服务器开启事务模式; 服务端会返回Tx.Select-Ok

  16. channel.basicPublish;发送消息,可以是多条,可以是消费消息提交ack
  17. channel.txCommit()提交事务;
  18. channel.txRollback()回滚事务;

消费者使用事务:

  1. autoAck=false,手动提交ack,以事务提交或回滚为准;
  2. autoAck=true,不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了

如果其中任意一个环节出现问题,就会抛出IoException异常,用户可以拦截异常进行事务回滚,或决定要不要重复消息。

事务消息会降低rabbitmq的性能

5、RabbitMQ死信队列、延时队列

  1. 消息被消费方否定确认,使用channel.basicNack 或 channel.basicReject ,并且此时 requeue 属性被设置为false 。
  2. 消息在队列的存活时间超过设置的TTL时间。
  3. 消息队列的消息数量已经超过最大队列长度。

那么该消息将成为“死信”。“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃

为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key,死信队列只不过是绑定在死信交换机上的队列,死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型 【Direct 、 Fanout 、Topic】
TTL:一条消息或者该队列中的所有消息的最大存活时间

如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。

只需要消费者一直消费死信队列里的消息

6、RabbitMQ镜像队列机制

镜像queue有master节点和slave节点。master和slave是针对一个queue而⾔的,而不是一个node作为 所有queue的master,其它node作为slave 。一个queue第一次创建的node为它的master节点,其它node为slave节点。
无论客户端的请求打到master还是slave最终数据都是从master节点获取。当请求打到master节点时,master节点直接将消息返回给client,同时master节点会通过GM ( Guaranteed Multicast) 协议将queue的最新状态广播到slave节点。GM保证了广播消息的原子性,即要么都更新要么都不更新。
当请求打到slave节点时,slave节点需要将请求先重定向到master节点,master节点将将消息返回给 client,同时master节点会通过GM协议将queue的最新状态广播到slave节点。
如果有新节点加入,RabbitMQ不会同步之前的历史数据,新节点只会复制该节点加入到集群之后新增的消息。

7、Kafka是什么

Kafka是一种高吞吐量、分布式、基于发布/订阅的消息系统,最初由LinkedIn公司开发,使用Scala 语⾔编写,目前是 Apache 的开源项目 。
broker:Kafka 服务器,负责消息存储和转发
topic:消息类别,Kafka按照topic来分类消息
partition:topic 的分区,一个topic 可以包含多个partition ,topic 消息保存在各个partition上
offset:消息在日志中的位置,可以理解是消息在 partition 上的偏移量,也是代表该消息的唯一序号
Producer:消息生产者
Consumer:消息消费者
Consumer Group:消 费者分组,每个Consumer 必须属于一个
groupZookeeper:保存着集群 broker、topic、partition 等meta数据;另外,还负责 broker 故障发现,partition leader 选举,负载均衡等功能

8、Kafka为什么吞吐量高

Kafka的生产者采用的是异步发送消息机制,当发送一条消息时,消息并没有发送到Broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送给Broker 。这种做法减少了网络io,从而提高了消息发送的吞吐量,但是如果消息生产者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了性能却降低了可靠性。

9、Kafka的Pull和Push分别有什么优缺点

  1. pull表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以pull可以由消费者自已控制,根据 自已的消息处理能力来进行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空
    2. push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按自已的 能力来消费消息,推过来多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞,消费者 压力大等问题

    10、为什么要使用kafka,为什么要使用消息队列?

    缓冲和削峰:上游数据时有突发流量,下游可能扛不住,或者下游没有足够多的机器来保证冗余,kafka在中间可以起到一个缓冲的作用,把消息暂存在kafka中,下游服务就可以按照自已的节奏进行慢慢处理。
    解耦和扩展性:项目开始的时候,并不能确定具体需求。消息队列可以作为一个接口层,解耦重要的业务流程。只需要遵守约定,针对数据编程即可获取扩展能力。
    冗余:可以采用一对多的方式,一个生产者发布消息,可以被多个订阅topic的服务消费到,供多个毫无关联的业务使用。
    健壮性:消息队列可 以堆积请求,所以消费端业务即使短时间死掉,也不会影响主要业务的正常进行。
    异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

    11、Kafka中的ISR、AR又代表什么? ISR的伸缩又指什么

    ISR:In-Sync Replicas 副本同步队列
    AR:Assigned Replicas 所有副本ISR是由leader维护,follower从leader同步数据有一些延迟 ( 包括延 迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x 中只支持replica.lag.time.max.ms这个维度) ,任意一个超过阈值都会把follower剔除出ISR, 存入 OSR ( Outof-Sync Replicas) 列表,新加入的follower也会先存放在OSR中 。AR=ISR+OSR。

    12、Kafka高效文件存储设计特点:

  2. Kafka 把topic 中一个parition 大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。
    2. 通过索引信息可以快速定位message 和确定response 的最大大小。
    3. 通过index 元数据全部映射到memory,可以避免segment file 的IO 磁盘操作。
    4. 通过索引文件稀疏存储,可以大幅降低index 文件元数据占用空间大小。

    13、Kafka与传统消息系统之间有三个关键区别

  3. Kafka 持久化日志,这些日志可以被重复读取和无限期保留
    2. Kafka 是一个分布式系统:它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能 力和高可用性
    3. Kafka 支持实时的流式处理

14、Kafka创建Topic 时如何将分区放置到不同的Broker 中

  1. 副本因子不能大于Broker 的个数;
    2. 第一个分区 ( 编号为 0) 的第一个副本放置位置是随机从 brokerList 选择的;
    3. 其他分区的第一个副本放置位置相对于第 0 个分区依次往后移 。也就是如果我们有 5 个Broker,
    5 个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上; 第 三个分区将会放在第一个 Broker 上; 第四个分区将会放在第二个Broker 上,依次类推;
    4. 剩余的副本相对于第一个副本放置位置其实是由nextReplicaShift 决定的,而这个数也是随机产生的

    15、Kafka的消费者如何消费数据

    消费者每次消费数据的时候,消费者都会记录消费的物理偏移量 ( offset) 的位置等到下次消费时,他 会接着上次位置继续消费

    16、Kafka消费者负载均衡策略

    一个消费者组中的一个分片对应一个消费者成员,他能保证每个消费者成员都能访问,如果组中成员太多会有空闲的成员

    17、kafaka生产数据时数据的分组策略

    生产者决定数据产生到集群的哪个partition 中每一条消息都是以 ( key,value) 格式 Key是由生产者发送数据传入所以生产者 ( key) 决定了数据产生到集群的哪个 partition

    18、Kafka中是怎么体现消息顺序性的?

    kafka每个partition中的消息在写入时都是有序的,消费时,每个partition只能被每一个group中的一个 消费者消费,保证了消费时也是有序的。整个topic不保证有序 。如果为了保证topic整个有序,那么将 partition调整为1.

    19、Kafka如何实现延迟队列?

    Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个 用于实现延迟功能的定时器 ( SystemTimer) 。JDK的Timer和DelayQueue插入和删除操作的平均时间 复杂度为O(nlog(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1) 。时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty 、Akka 、Quartz 、Zookeeper等组件中都存在时间轮的踪影。底层使用数组实现,数组中的每个元素可以存放一个TimerTaskList对象 。TimerTaskList是一个环形双向链表,在其中的链表项TimerTaskEntry中封装了真正的定时任务TimerTask.Kafka中到底是怎么推进时间的呢? Kafka中的定时器借助了JDK中的DelayQueue来协助推进时间轮。具体做法是对于每个使用到的TimerTaskList都会加入到DelayQueue 中。Kafka中的TimingWheel专⻔用来执行插入和删除TimerTaskEntry的操作,而DelayQueue专⻔负 责时间推进的任务。再试想一下,DelayQueue中的第一个超时任务列表的expiration为200ms,第二个 超时任务为840ms,这里获取DelayQueue的队头只需要O(1)的时间复杂度 。如果采用每秒定时推进 , 那么获取到第一个超时的任务列表时执行的200次推进中有199次属于“空推进”,而获取到第二个超时 任务时有需要执行639次“空推进”,这样会无故空耗机器的性能资源,这里采用DelayQueue来辅助以少 量空间换时间,从而做到了“精准推进”。Kafka中的定时器真可谓是“知人善用”,用TimingWheel做最 擅长的任务添加和删除操作,而用DelayQueue做最擅长的时间推进工作,相辅相成。

    20、RocketMQ的事务消息是如何实现的

    image.png

  2. 生产者订单系统先发送一条half消息到Broker,half消息对消费者而⾔是不可见的

  3. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback
  4. 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功
  5. 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束
  6. 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理

    21、为什么RocketMQ不使用Zookeeper作为注册中心呢?

    根据CAP理论,同时最多只能满足两个点,而zookeeper满足的是CP,也就是说zookeeper并不能保证 服务的可用性,zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。
    基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集 群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决这个问题只能通过划分领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致 服务之间是不连通的。
    持久化的机制来带的问题,ZooKeeper 的ZAB 协议对每一个写请求,会在每个ZooKeeper节点上保持写一个事务日志,同时再加上定期的将内存数据镜像 ( Snapshot) 到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
    消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时 候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用 ,短时间内对于生 产者和消费者并不会产生太大影响。

    22、RocketMQ的实现原理

    RocketMQ由NameServer注册中心集群 、Producer生产者集群 、Consumer消费者集群和若⼲ Broker ( RocketMQ进程) 组成,它的架构原理是这样的:
    Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
    Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器 来发送消息
    Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费

    23、RocketMQ为什么速度快

    因为使用了顺序存储、Page Cache和异步刷盘。我们在写入commitlog的时候是顺序写入的,这样比 随机写入的性能就会提高很多,写入commitlog的时候并不是直接写入磁盘,而是先写入操作系统的 PageCache,最后由操作系统异步将缓存中的数据刷到磁盘

    24、消息队列如何保证消息可靠传输

    消息可靠传输代表了两层意思,既不能多也不能少。

  7. 为了保证消息不多,也就是消息不能重复,也就是生产者不能重复生产消息,或者消费者不能重复消费消息

  8. 首先要确保消息不多发,这个不常出现,也比较难控制,因为如果出现了多发,很大的原因是生产者自已的原因,如果要避免出现问题,就需要在消费端做控制
  9. 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决生产者重复发送消息的问题
  10. 消息不能少,意思就是消息不能丢失,生产者发送的消息,消费者一定要能消费到,对于这个问题,就要考虑两个方面
  11. 生产者发送消息时,要确认broker确实收到并持久化了这条消息,比如RabbitMQ的confirm机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给broker
  12. broker要等待消费者真正确认消费到了消息时才删除掉消息,这里通常就是消费端ack机制,消费者接收到一条消息后,如果确认没问题了,就可以给broker发送一个ack,broker接收到ack后才会删除消息

    25、消息队列有哪些作用

  13. 解耦:使用消息队列来作为两个系统之间的通讯方式,两个系统不需要相互依赖了

  14. 异步:系统A给消息队列发送完消息之后,就可以继续做其他事情了
  15. 流量削峰:如果使用消息队列的方式来调用某个系统,那么消息将在队列中排队,由消费者自已控制消费速度

    26、死信队列是什么?延时队列是什么?

  16. 死信队列也是一个消息队列,它是用来存放那些没有成功消费的消息的,通常可以用来作为消息重试

  17. 延时队列就是用来存放需要在指定时间被处理的元素的队列,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付则取消订单

    27、如何保证消息的高效读写?

    零拷贝:kafka和RocketMQ都是通过零拷贝技术来优化文件读写。
    传统文件复制方式: 需要对文件在内存中进行四次拷贝。
    零拷贝:有两种方式,mmap和transfile ,Java当中对零拷贝进行了封装,Mmap方式通过 MappedByteBuffer对象进行操作,而transfile通过FileChannel来进行操作。Mmap 适合比较小的文件,通常文件大小不要超过1.5G ~2G 之间。Transfile没有文件大小限制。RocketMQ当中使用Mmap方 式来对他的文件进行读写。
    在kafka当中,他的index日志文件也是通过mmap的方式来读写的 。在其他日志文件当中,并没有使用 零拷贝的方式。Kafka使用transfile方式将硬盘数据加载到网卡。

    28、让你设计一个MQ,你会如何设计?

    两个误区:

  18. 放⻜自我,漫无边际。

  19. 纠结技术细节。

好的方式:

  1. 从整体到细节,从业务场景到技术实现。
  2. 以现有产品为基础。

答题思路: MQ作用 、项目大概的样子 。

  1. 实现一个单机的队列数据结构。 高效、可扩展。
  2. 将单机队列扩展成为分布式队列。- 分布式集群管理
  3. 基于Topic定制消息路由策略。- 发送者路由策略,消费者与队列对应关系,消费者路由策略
  4. 实现高效的网络通信。- Netty Http
  5. 规划日志文件,实现文件高效读写。- 零拷贝,顺序写。服务重启后,快速还原运行现场。
  6. 定制高级功能,死信队列、延迟队列、事务消息等等。 - 贴合实际,随意发挥。

    三、网络(11)

    1、什么是认证和授权?如何设计一个权限认证框架?

    认证:就是对系统访问者的身份进行确认。
    授权:就是对系统访问者的行为进行控制。授权通常是在认证之后,对系统内的用户隐私数据进行保护。后台接口访问权限、前台控件的访问权限。
    RBAC模型:主体 -> ⻆色 -> 资源 -> 访问系统的行为。
    认证和授权也是对一个权限认证框架进行扩展的两个主要的方面。

    2、如果没有Cookie、Session还能进行身份验证吗?

    当服务器Tomcat第一次接收到客户端的请求时,会开辟一块独立的Session空间,建立一个Session对 象,同时会生成一个Session id,通过响应头的方式保存到客户端浏览器的Cookie当中。以后客户端的每次请求,都会在请求头部带上这个Session id,这样就可以对应上服务端的一些会话的相关信息,比如用户的登录状态。
    如果没有客户端的Cookie,Session是无法进行身份验证的。

当服务端从单体应用升级为分布式之后,Cookie + Session这种机制要怎么扩展?

  1. Session黏贴: 在负载均衡中,通过一个机制保证同一个客户端的所有请求都会转发到同一个 Tomcat实例当中。问题: 当这个Tomcat实例出现问题之后,请求就会被转发到其他实例,这时候用户的Session信息就丢了。
  2. Session复制: 当一个Tomcat实例上保存了Session信息后,主动将Session复制到集群中的其他实例。问题: 复制是需要时间的,在复制过程中,容易产生Session信息丢失。
  3. Session共享: 就是将服务端的Session信息保存到一个第三方中,比如Redis。

    3、什么是CSRF攻击?如何防止?

    CSRF: Cross Site Requst Forgery 跨站请求伪造,一个正常的请求会将合法用户的Session id保存到浏览器的Cookie。这时候,如果用户在浏览器中打来另一个tab页,那这个tab页也是可以获得浏览器的Cookie 。黑客就可以利用这个Cookie信息进行攻击。
    攻击过程:

  4. 某银行网站A可以以GET请求的方式发起转账操作。www.xxx.com/transfor.do? accountNum=100&money=1000 accountNum表示目标账户。这个请求肯定是需要登录才可以正常访问的。

  5. 攻击者在某个论坛或者网站上,上传一个图片,链接地址是www.xxx.com/transfer.do? accountNum=888&money=10000 其中这个accountNum就是攻击者自已的银行账户。
  6. 如果有一个用户,登录了银行网站,然后又打开浏览器的另一个tab页,点击了这个图片。这时,银行就会受理到一个带了正确Cookie的请求,就会完成转账。用户的钱就被盗了。

CSRF防止方式:

  1. 尽量使用POST请求,限制GET请求。POST请求可以带请求体,攻击者就不容易伪造出请求。
  2. 将Cookie设置为HttpOnly : respose.setHeader(“Set- Cookie”,”Cookiename=Cookievalue;HttpOnly”)。
  3. 增加token;
  4. 在请求中放入一个攻击者无法伪造的信息,并且该信息不存在于Cookie当中。这也是Spring Security框架中采用的防范方式。

    4、什么是OAuth2.0协议?有哪几种认证方式?

    OAuth2.0是一个开放标准,允许用户授权第三方应用程序访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
    OAuth2.0协议的认证流程,简单理解,就是允许我们将之前的授权和认证过程交给一个独立的第三方进行担保。
    OAuth2.0协议有四种认证方式:

  5. 授权码模式

  6. 简化模式
  7. 密码模式
  8. 客户端模式

    5、什么是SSO?与OAuth2.0有什么关系?

    OAuth2.0的使用场景通常称为联合登录,一处注册 ,多处使用
    SSO Single Sign On 单点登录。 一处登录,多处同时登录
    SSO的实现关键是将Session信息集中存储

    6、如何设计一个开放授权平台?

    开放授权平台也可以按照认证和授权两个方向来梳理。

  9. 认证: 就可以按照OAuth2.0协议来规划认证的过程。

  10. 授权:

    1. 首先需要待接入的第三方应用在开放授权平台进行注册,注册需要提供几个必要的信息clintID,消息推送地址,密钥(一对公私钥,私钥由授权平台自已保存,公钥分发给第三方应用)
    2. 然后,第三方应用引导客户发起请求时,采用公钥进行参数加密,授权开放平台使用对应的私钥解密。
    3. 接下来:授权开放平台同步响应第三方应用的只是消息是否处理成功的结果。而真正的业务数据由授权开放平台异步推动给第三方应用预留的推送地址。

      7、epoll和poll的区别

  11. select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件

  12. poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件
  13. epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询

    8、TCP的三次握手和四次挥手

    TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。
    在建立TCP连接时,需要通过三次握手来建立,过程是:

  14. 客户端向服务端发送一个SYN

  15. 服务端接收到SYN后,给客户端发送一个SYN_ACK
  16. 客户端接收到SYN_ACK后,再给服务端发送一个ACK

在断开TCP连接时,需要通过四次挥手来断开,过程是:

  1. 客户端向服务端发送FIN
  2. 服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
  3. 服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
  4. 客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了

    9、浏览器发出一个请求到收到响应经历了哪些步骤?

  5. 浏览器解析用户输入的URL,生成一个HTTP格式的请求

  6. 先根据URL域名从本地hosts文件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到IP地址
  7. 浏览器通过操作系统将请求通过四层网络协议发送出去
  8. 途中可能会经过各种路由器、交换机,最终到达服务器
  9. 服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被 Tomcat占用了
  10. Tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
  11. 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的Controller中的方法,并执行该方法得到结果
  12. Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
  13. 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染

    10、跨域请求是什么?有什么问题?怎么解决?

    跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则浏览器会进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问
    www.jd.com是不行的,但是如果是img 、iframe 、script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了用户信息安全。但是如果开发者想要绕过这层限制也是可以的:

  14. response添加header,比如resp.setHeader(“Access-Control-Allow-Origin”, “*”);表示可以访问 所有网站,不受是否同源的限制

  15. jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
  16. 后台自已控制,先访问同域名下的接口,然后在接口中再去使用HTTPClient等工具去调用目标接口
  17. 网关,和第三种方式类似,都是交给后台服务来进行跨域访问

    11、零拷贝是什么

    零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,而直接实现转移。
    image.pngimage.png

四、Leetcode算法(10)

1、反转链表

反转一个单链表。

  1. 输入: 1->2->3->4->5
  2. 输出: 5->4->3->2->1

解法1:迭代,重复某一过程,每一次处理结果作为下一次处理的初始值,这些初始值类似于状态、每次处理都会改变状态、直至到达最终状态
从前往后遍历链表,将当前节点的next指向上一个节点,因此需要一个变量存储上一个节点prev,当前节点处理完需要寻找下一个节点,因此需要一个变量保存当前节点curr,处理完后要将当前节点赋值给prev,并将next指针赋值给curr,因此需要一个变量提前保存下一个节点的指针next
image.png

  1. 将下一个节点指针保存到next变量 next = curr.next
  2. 将下一个节点的指针指向prev,curr.next = prev
  3. 准备处理下一个节点,将curr赋值给prev
  4. 将下一个节点赋值为curr,处理一个节点

解法2:递归:以相似的方法重复,类似于树结构,先从根节点找到叶子节点,从叶子节点开始遍历大的问题(整个链表反转)拆成性质相同的小问题(两个元素反转)curr.next.next = curr
将所有的小问题解决,大问题即解决
image.png
只需每个元素都执行curr.next.next = curr,curr.next = null两个步骤即可为了保证链不断,必须从最后一个元素开始

  1. public class ReverseList {
  2. static class ListNode{
  3. int val;
  4. ListNode next;
  5. public ListNode(int val, ListNode next) {
  6. t his.val = val;
  7. this.next = next;
  8. }
  9. }
  10. public static ListNode iterate(ListNode head){
  11. ListNode prev = null,curr,next;
  12. curr = head;
  13. while(curr != null){
  14. next = curr.next;
  15. curr.next = prev;
  16. prev = curr;
  17. curr = next;
  18. }
  19. return prev;
  20. }
  21. public static ListNode recursion(ListNode head) {
  22. if (head == null || head.next == null) {
  23. return head;
  24. }
  25. ListNode newHead = recursion(head.next);
  26. head.next.next = head;
  27. head.next = null;
  28. return newHead;
  29. }
  30. public static void main(String[] args) {
  31. ListNode node5 = new ListNode(5,null);
  32. ListNode node4 = new ListNode(4,node5);
  33. ListNode node3 = new ListNode(3,node4);
  34. ListNode node2 = new ListNode(2,node3);
  35. ListNode node1 = new ListNode(1,node2);
  36. //ListNode node = iterate(node1); ListNode node_1 = recursion(node1);
  37. System.out.println(node_1);
  38. }
  39. }

2、统计N以内的素数

素数:只能被1和自身整除的数,0、1除外
解法一:暴力算法
直接从2开始遍历,判断是否能被2到自身之间的数整除

  1. public int countPrimes(int n) {
  2. int ans = 0;
  3. for (int i = 2; i < n; ++i) {
  4. ans += isPrime(i) ? 1 : 0;
  5. }
  6. return ans;
  7. }
  8. //i如果能被x整除,则x/i肯定能被x整除,因此只需判断i和根号x之中较⼩的即可
  9. public boolean isPrime(int x) {
  10. for (int i = 2; i * i <= x; ++i) {
  11. if (x % i == 0) {
  12. return false;
  13. }
  14. }
  15. return true;
  16. }

解法2:埃氏筛
利用合数的概念(非素数),素数*n必然是合数,因此可以从2开始遍历,将所有的合数做上标记

  1. public static int eratosthenes(int n) {
  2. boolean[] isPrime = new boolean[n];
  3. int ans = 0;
  4. for (int i = 2; i < n; i++) {
  5. if (!isPrime[i]) {
  6. ans += 1;
  7. for (int j = i * i; j < n; j += i) {
  8. isPrime[j] = true;
  9. }
  10. }
  11. }
  12. return ans;
  13. }

将合数标记为true ,j = i i 从 2 i 优化而来,系数2会随着遍历递增(j += i,相当于递增了系数 2),每一个合数都会有两个比本身要小的因子(0,1除外),2 * i 必然会遍历到这两个因子

当2递增到大于根号n时,其实后面的已经无需再判断(或者只需判断后面一段),而2到根号n 、实际上在i递增的过程中已经计算过了,i实际上就相当于根号n
例如:n = 25 会计算以下
2 4 = 8
3
4 = 12
但实际上8和12已经标记过,在n = 17时已经计算了 3 4,2 4

3、寻找数组的中心索引

数组中某一个下标,左右两边的元素之后相等,该下标即为中心索引思路:先统计出整个数组的总和,然后从第一个元素开始叠加
总和递减当前元素,叠加递增当前元素,知道两个值相等

  1. public static int pivotIndex(int[] nums) {
  2. int sum1 = Arrays.stream(nums).sum();
  3. int sum2 = 0;
  4. for(int i = 0; i < nums.length; i++){
  5. sum2 += nums[i];
  6. if(sum1 == sum2){
  7. return i;
  8. }
  9. sum1 = sum1 - nums[i];
  10. }
  11. return -1;
  12. }

4、删除排序数组中的重复项

一个有序数组nums ,原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。
不要使用额外的数组空间,必须在原地修改输入数组并在使用O(1) 额外空间的条件下完成。

双指针算法:
数组完成排序后,我们可以放置两个指针 i 和 j,其中 i 是慢指针,而 j 是快指针 。只要nums[i]=nums[j],我们就增加 j 以跳过重复项。
当遇到nums[j] != nums[i]时,跳过重复项的运行已经结束,必须把nums[j]) 的值复制到 nums[i + 1] 。然后递增 i,接着将再次重复相同的过程,直到 j 到达数组的末尾为止 。

  1. public int removeDuplicates(int[] nums) {
  2. if (nums.length == 0)
  3. return 0;
  4. int i = 0;
  5. for (int j = 1; j < nums.length; j++) {
  6. if (nums[j] != nums[i]) {
  7. i++; nums[i] = nums[j];
  8. }
  9. }
  10. return i + 1;
  11. }

5、x的平方根

在不使用 sqrt(x) 函数的情况下,得到 x的平方根的整数部分
解法一:二分查找
x的平方根肯定在0到x之间,使用二分查找定位该数字,该数字的平方一定是最接近x的,m平方值如果大于x 、则往左边找,如果小于等于x则往右边找
找到0和X的最中间的数m,
如果m m > x,则m取x/2到x的中间数字,直到m m < x,m则为平方根的整数部分
如果m * m <= x,则取0到x/2的中间值,知道两边的界限重合,找到最大的整数,则为x平方根的整数部分
时间复杂度:O(logN)

  1. public static int binarySearch(int x) {
  2. int l = 0, r = x, index = -1;
  3. while (l <= r) {
  4. int mid = l + (r - l) / 2;
  5. if ((long) mid * mid <= x) {
  6. index = mid;
  7. l = mid + 1;
  8. } else {
  9. r = mid - 1;
  10. }
  11. }
  12. return index;
  13. }

解法二:牛顿迭代
假设平方根是 i,则 i和 x/i必然都是x的因子,而 x/i必然等于 i,推导出 i + x/i = 2*i,得出 i = (i + x/i)/2
由此得出解法,i 可以任选一个值,只要上述公式成立,i 必然就是x的平方根,如果不成立,(i + x / i)/2得出的值进行递归,直至得出正确解

  1. public static int newton(int x) {
  2. if(x==0) return 0;
  3. return ((int)(sqrts(x,x)));
  4. }
  5. public static double sqrts(double i,int x){
  6. double res = (i + x / i) / 2;
  7. if (res == i) {
  8. return i;
  9. } else {
  10. return sqrts(res,x);
  11. }
  12. }

6、三个数的最大乘积

一个整型数组乘积不会越界,在数组中找出由三个数字组成的最大乘积,并输出这个乘积。
如果数组中全是非负数,则排序后最大的三个数相乘即为最大乘积; 如果全是非正数,则最大的三个数
相乘同样也为最大乘积。
如果数组中有正数有负数,则最大乘积既可能是三个最大正数的乘积,也可能是两个最小负数 ( 即绝对值最大) 与最大正数的乘积。
分别求出三个最大正数的乘积,以及两个最小负数与最大正数的乘积,二者之间的最大值即为所求答。
解法一:排序

  1. public static int sort(int[] nums) {
  2. Arrays.sort(nums);
  3. int n = nums.length;
  4. return Math.max(nums[0] * nums[1] * nums[n - 1], nums[n - 3] * nums[n - 2] * nums[n - 1]);
  5. }

解法二:线性扫描

  1. public static int getMaxMin(int[] nums) {
  2. // 最⼩的和第⼆⼩的 int min1 = 0, min2 = 0;
  3. // 最⼤的、第⼆⼤的和第三⼤的 int max1 = 0, max2 = 0, max3 = 0;
  4. for (int x : nums) {
  5. if (x < min1) {
  6. min2 = min1;
  7. min1 = x;
  8. } else if (x < min2) {
  9. min2 = x;
  10. }if (x > max1) {
  11. max3 = max2;
  12. max2 = max1;
  13. max1 = x;
  14. } else if (x > max2) {
  15. max3 = max2;
  16. max2 = x;
  17. } else if (x > max3) {
  18. max3 = x;
  19. }
  20. }
  21. }

7、两数之和

给定一个升序排列的整数数组numbers ,从数组中找出两个数满足相加之和等于目标数target 。假设每个输入只对应唯一的答案,而且不可以重复使用相同的元素。
返回两数的下标值,以数组形式返回暴力解法

  1. public int[] twoSum(int[] nums, int target) {
  2. int n = nums.length;
  3. for (int i = 0; i < n; ++i) {
  4. for (int j = i + 1; j < n; ++j) {
  5. if (nums[i] + nums[j] == target) {
  6. return new int[]{i, j};
  7. }
  8. }
  9. }
  10. return new int[0];
  11. }

时间复杂度:O(N的平方) 空间复杂度:O(1)
哈希表:将数组的值作为key存入map ,target - num作为key

  1. public int[] twoSum(int[] nums, int target) {
  2. Map<Integer, Integer> map = new HashMap<Integer, Integer>();
  3. for (int i = 0; i < nums.length; ++i) {
  4. if (map.containsKey(target - nums[i])) {
  5. return new int[]{map.get(target - nums[i]), i};
  6. }
  7. map.put(nums[i], i);
  8. }
  9. return new int[0];
  10. }

时间复杂度:O(N) 空间复杂度:O(N)

解法一:二分查找
先固定一个值(从下标0开始),再用二分查找查另外一个值,找不到则固定值向右移动,继续二分查找

  1. public int[] twoSearch(int[] numbers, int target) {
  2. for (int i = 0; i < numbers.length; ++i) {
  3. int low = i, high = numbers.length -1;
  4. while (low <= high) {
  5. int mid = (high - low) / 2 + low;
  6. if (numbers[mid] == target - numbers[i]) {
  7. return new int[]{i, mid};
  8. } else if (numbers[mid] > target - numbers[i]) {
  9. high = mid - 1;
  10. } else {
  11. low = mid + 1;
  12. }
  13. }
  14. }
  15. }

时间复杂度:O(N logN)
空间复杂度:O(1)
*解法二:双指针

左指针指向数组head,右指针指向数组tail,head+tail > target 则tail 左移,否则head右移

  1. public int[] twoPoint(int[] numbers, int target) {
  2. int low = 0, high = numbers.length - 1;
  3. while (low < high) {
  4. int sum = numbers[low] + numbers[high];
  5. if (sum == target) {
  6. return new int[]{low + 1, high + 1};
  7. } else if (sum < target) {
  8. ++low;
  9. } else {
  10. --high;
  11. }
  12. }
  13. return new int[]{-1, -1};
  14. }

时间复杂度:O(N) 空间复杂度:O(1)

8、斐波那契数列

求取斐波那契数列第N位的值。
斐波那契数列:每一位的值等于他前两位数字之和。前两位固定 0,1,1,2,3,5,8 。。。。
解法一:暴力递归
image.png

  1. public static int calculate(int num){
  2. if(num == 0 ){
  3. return 0;
  4. }
  5. if(num == 1){
  6. return 1;
  7. }
  8. return calculate(num-1) + calculate(num-2);
  9. }

解法二:去重递归
递归得出具体数值之后、存储到一个集合(下标与数列下标一致),后面递归之前先到该集合查询一次,如果查到则无需递归、直接取值。查不到再进行递归计算
image.png

  1. public static int calculate2(int num){
  2. int[] arr = new int[num+1];
  3. return recurse(arr,num);
  4. }
  5. private static int recurse(int[] arr, int num) {
  6. if(num == 0 ){
  7. return 0;
  8. }
  9. if(num == 1){
  10. return 1;
  11. }
  12. if(arr[num] != 0){
  13. return arr[num];
  14. }
  15. arr[num] = recurse(arr,num-1) + recurse(arr,num-2);
  16. return arr[num];
  17. }

解法三:双指针迭代
基于去重递归优化,集合没有必要保存每一个下标值,只需保存前两位即可,向后遍历,得出N的值
image.png

  1. public static int iterate(int num){
  2. if(num == 0 ){
  3. return 0;
  4. }
  5. if(num == 1){
  6. return 1;
  7. }
  8. int low = 0,high = 1;
  9. for(int i=2; i<= num; i++){
  10. int sum = low + high; low = high;
  11. high = sum;
  12. }
  13. return high;
  14. }

9、环形链表

给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next 指针再次到达该节点,则链表中存在环如果链表中存在环,则返回 true 。 否则,返回 false 。
解法一:哈希表

  1. public static boolean hasCycle(ListNode head) {
  2. Set<ListNode> seen = new HashSet<ListNode>();
  3. while (head != null) {
  4. if (!seen.add(head)) {
  5. return true;
  6. }
  7. head = head.next;
  8. }
  9. return false;
  10. }

解法二:双指针

  1. public static boolean hasCycle2(ListNode head) {
  2. if (head == null || head.next == null) {
  3. return false;
  4. }
  5. ListNode slow = head; ListNode fast = head.next;
  6. while (slow != fast) {
  7. if (fast == null || fast.next == null) {
  8. return false;
  9. }
  10. slow = slow.next; fast = fast.next.next;
  11. }
  12. return true;
  13. }

10、排列硬币

总共有n枚硬币,将它们摆成一个阶梯形状,第 k行就必须正好有 k枚硬币 。给定一个数字 n,找出可形成完整阶梯行的总行数。
n是一个非负整数,并且在32位有符号整型的范围内
解法一:迭代
从第一行开始排列,排完一列、计算剩余硬币数,排第二列,直至剩余硬币数小于或等于行数

  1. public static int arrangeCoins(int n) {
  2. for(int i=1; i<=n; i++){
  3. n = n-i;
  4. if (n <= i){
  5. return i;
  6. }
  7. }
  8. return 0;
  9. }

解法二:二分查找
假设能排 n 行,计算 n 行需要多少硬币数,如果大于 n,则排 n/2行,再计算硬币数和 n 的大小关系

  1. public static int arrangeCoins2(int n) {
  2. int low = 0, high = n;
  3. while (low <= high) {
  4. long mid = (high - low) / 2 + low; long cost = ((mid + 1) * mid) / 2;
  5. if (cost == n) {
  6. return (int)mid;
  7. } else if (cost > n) {
  8. high = (int)mid - 1;
  9. } else {
  10. low = (int)mid + 1;
  11. }
  12. }
  13. return high;
  14. }

解法三:牛顿迭代
使用牛顿迭代求平方根,(x + n/x)/2
假设能排x 行则 1 + 2 + 3 + …+ x = n,即x(x+1)/2 = n 推导出 x = 2n - x

  1. public static double sqrts(double x,int n){
  2. double res = (x + (2*n-x) / x) / 2;
  3. if (res == x) {
  4. return x;
  5. } else {
  6. return sqrts(res,n);
  7. }
  8. }