Java面试指导

我们是做就业服务的工作室,没有任何培训机构性质!!
主做Java、python、c++,前端vue,react等,
全国各地,简历包装,投递邀约,视频面试,技术面试包通过,离职背调等,
通过正常上班,不拿offer不收费,
不要浪费投递简历的机会和面试机会,
如果已经在职的话,并且不满意目前薪资也可以联系我们。关注公众号回复“就业”即可。

image-20241203185525611

或者添加微信咨询:
添加微信

分布式与微服务

什么是CAP理论

CAP理论是分布式领域中⾮常重要的⼀个指导理论,C(Consistency)表示强⼀致性, A(Availability)表示可⽤性,P(Partition Tolerance)表示分区容错性,CAP理论指出在⽬前的硬件 条件下,⼀个分布式系统是必须要保证分区容错性的,⽽在这个前提下,分布式系统要么保证CP,要么 保证AP,⽆法同时保证CAP。

分区容错性表示,⼀个系统虽然是分布式的,但是对外看上去应该是⼀个整体,不能由于分布式系统内 部的某个结点挂点,或⽹络出现了故障,⽽导致系统对外出现异常。所以,对于分布式系统⽽⾔是⼀定 要保证分区容错性的。

强⼀致性表示,⼀个分布式系统中各个结点之间能及时的同步数据,在数据同步过程中,是不能对外提 供服务的,不然就会造成数据不⼀致,所以强⼀致性和可⽤性是不能同时满⾜的。

可⽤性表示,⼀个分布式系统对外要保证可⽤。

什么是BASE理论

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

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

什么是RPC

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

数据⼀致性模型有哪些

  • 强⼀致性:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值,这种是对 ⽤户 最友好的,就是⽤户上⼀次写什么,下⼀次就保证能读到什么。根据 CAP理论,这种实现需 要牺牲可⽤性。
  • 弱⼀致性:系统在数据写⼊成功之后,不承诺⽴即可以读到最新写⼊的值,也不会具体的承诺多久 之后 可以读到。⽤户读到某⼀操作对系统数据的更新需要⼀段时间,我们称这段时间为“不⼀致性 窗⼝”。
  • 最终⼀致性:最终⼀致性是弱⼀致性的特例,强调的是所有的数据副本,在经过⼀段时间的同步之 后, 最终都能够达到⼀个⼀致的状态。因此,最终⼀致性的本质是需要系统保证最终数据能够达 到⼀致,⽽ 不需要实时保证系统数据的强⼀致性。到达最终⼀致性的时间 ,就是不⼀致窗⼝时 间,在没有故障发⽣的前提下,不⼀致窗⼝的时间主要受通信延迟,系统负载和复制副本的个数影 响。最终⼀致性模型根据其提供的不同保证可以划分为更多的模型,包括因果⼀致性和会话⼀致性 等。

分布式ID是什么?有哪些解决⽅案?

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

  1. uuid,这种⽅案复杂度最低,但是会影响存储空间和性能
  2. 利⽤单机数据库的⾃增主键,作为分布式ID的⽣成器,复杂度适中,ID⻓度较之uuid更短,但是受 到单机数据库性能的限制,并发量⼤的时候,此⽅案也不是最优⽅案
  3. 利⽤redis、zookeeper的特性来⽣成id,⽐如redis的⾃增命令、zookeeper的顺序节点,这种⽅案 和单机数据库(mysql)相⽐,性能有所提⾼,可以适当选⽤
  4. 雪花算法,⼀切问题如果能直接⽤算法解决,那就是最合适的,利⽤雪花算法也可以⽣成分布式 ID,底层原理就是通过某台机器在某⼀毫秒内对某⼀个数字⾃增,这种⽅案也能保证分布式架构中 的系统id唯⼀,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。

分布式锁的使⽤场景是什么?有哪些实现⽅案?

在单体架构中,多个线程都是属于同⼀个进程的,所以在线程并发执⾏时,遇到资源竞争时,可以利⽤ ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使⽤。

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

⽬前主流的分布式锁的实现⽅案有两种:

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

什么是分布式事务?有哪些实现⽅案?

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

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

  2. 消息队列:⽬前RocketMQ中⽀持事务消息,它的⼯作原理是:

    a. ⽣产者订单系统先发送⼀条half消息到Broker,half消息对消费者⽽⾔是不可⻅的

    b. 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback

    c. 并且⽣产者订单系统还可以提供Broker回调接⼝,当Broker发现⼀段时间half消息没有收到任 何操作命令,则会主动调此接⼝来查询订单是否创建成功

    d. ⼀旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事 务成功结束

    e. 如果消费失败,则根据重试策略进⾏重试,最后还失败则进⼊死信队列,等待进⼀步处理

  3. Seata:阿⾥开源的分布式事务框架,⽀持AT、TCC等多种模式,底层都是基于两阶段提交理论来 实现的

什么是ZAB协议

ZAB协议是Zookeeper⽤来实现⼀致性的原⼦⼴播协议,该协议描述了Zookeeper是如何实现⼀致性 的,分为三个阶段:

  1. 领导者选举阶段:从Zookeeper集群中选出⼀个节点作为Leader,所有的写请求都会由Leader节点 来处理
  2. 数据同步阶段:集群中所有节点中的数据要和Leader节点保持⼀致,如果不⼀致则要进⾏同步
  3. 请求⼴播阶段:当Leader节点接收到写请求时,会利⽤两阶段提交来⼴播该写请求,使得写请求像 事务⼀样在其他节点上执⾏,达到节点上的数据实时⼀致

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

简述paxos算法

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

Proposer提议者:只要Proposer发的提案Propose被半数以上的Acceptor接受,Proposer就认为该提 案例的value被选定了。

Acceptor接受者:只要Acceptor接受了某个提案,Acceptor就认为该提案例的value被选定了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资格。

简述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肯定有最新的⽇志

安全原则:

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

为什么Zookeeper可以⽤来作为注册中⼼

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

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就选举出来了

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

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

Dubbo⽀持哪些负载均衡策略

  1. 随机:从多个服务提供者随机选择⼀个来处理本次请求,调⽤量越⼤则分布越均匀,并⽀持按权重 设置随机概率
  2. 轮询:依次选择服务提供者来处理请求, 并⽀持按权重进⾏轮询,底层采⽤的是平滑加权轮询算法
  3. 最⼩活跃调⽤数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最⼩的服务器 来处理
  4. ⼀致性哈希:相同参数的请求总是发到同⼀个服务提供者

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

  1. ⾸先Dubbo会将程序员所使⽤的@DubboService注解或@Service注解进⾏解析得到程序员所定义 的服务参数,包括定义的服务名、服务接⼝、服务超时时间、服务协议等等,得到⼀个 ServiceBean。
  2. 然后调⽤ServiceBean的export⽅法进⾏服务导出
  3. 然后将服务信息注册到注册中⼼,如果有多个协议,多个注册中⼼,那就将服务按单个协议,单个 注册中⼼进⾏注册
  4. 将服务信息注册到注册中⼼后,还会绑定⼀些监听器,监听动态配置中⼼的变更
  5. 还会根据服务协议启动对应的Web服务器或⽹络框架,⽐如Tomcat、Netty等

Dubbo是如何完成服务引⼊的?

  1. 当程序员使⽤@Reference注解来引⼊⼀个服务时,Dubbo会将注解和服务的信息解析出来,得到 当前所引⽤的服务名、服务接⼝是什么
  2. 然后从注册中⼼进⾏查询服务信息,得到服务的提供者信息,并存在消费端的服务⽬录中
  3. 并绑定⼀些监听器⽤来监听动态配置中⼼的变更
  4. 然后根据查询得到的服务提供者信息⽣成⼀个服务接⼝的代理对象,并放⼊Spring容器中作为Bean

Dubbo的架构设计是怎样的?

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

  1. Proxy服务代理层,⽀持JDK动态代理、javassist等代理机制
  2. Registry注册中⼼层,⽀持Zookeeper、Redis等作为注册中⼼
  3. Protocol远程调⽤层,⽀持Dubbo、Http等调⽤协议
  4. Transport⽹络传输层,⽀持netty、mina等⽹络传输框架
  5. Serialize数据序列化层,⽀持JSON、Hessian等序列化机制

各层说明

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

关系说明

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

负载均衡算法有哪些

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

分布式架构下,Session 共享有什么⽅案

  1. 采⽤⽆状态服务,抛弃session

  2. 存⼊cookie(有安全⻛险)

  3. 服务器之间进⾏ Session 同步,这样可以保证每个服务器上都有全部的 Session 信息,不过当服务 器数量⽐较多的时候,同步是会有延迟甚⾄同步失败;

  4. IP 绑定策略

    使⽤ Nginx (或其他复杂均衡软硬件)中的 IP 绑定策略,同⼀个 IP 只能在指定的同⼀个机器访问,但 是这样做失去了负载均衡的意义,当挂掉⼀台服务器的时候,会影响⼀批⽤户的使⽤,⻛险很⼤;

  5. 使⽤ Redis 存储

    把 Session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问⼀次 Redis ,但是这种⽅案带 来的好处也是很⼤的:

    • 实现了 Session 共享;

    • 可以⽔平扩展(增加 Redis 服务器);

    • 服务器重启 Session 不丢失(不过也要注意 Session 在 Redis 中的刷新/失效机制);

    • 不仅可以跨服务器 Session 共享,甚⾄可以跨平台(例如⽹⻚端和 APP 端)。

简述你对RPC、RMI的理解

RPC:在本地调⽤远程的函数,远程过程调⽤,可以跨语⾔实现 httpClient

RMI:远程⽅法调⽤,java中⽤于实现RPC的⼀种机制,RPC的java版本,是J2EE的⽹络调⽤机制,跨 JVM调⽤对象的⽅法,⾯向对象的思维⽅式

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

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

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

Class[] interfaces = stubClass.getInterfaces();
for (Class c : interfaces) {
System.out.println("implement" + c.getName() + " interface");
}
System.out.println(service02.service("hello"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

如何实现接⼝的幂等性

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

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上被删除 。

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

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

命名服务:

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

配置管理:

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

集群管理:

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

讲下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回调是串⾏同步的

Zookeeper和Eureka的区别

zk:CP设计(强⼀致性),⽬标是⼀个分布式的协调系统,⽤于进⾏资源的统⼀管理。

当节点crash后,需要进⾏leader的选举,在这个期间内,zk服务是不可⽤的。

eureka:AP设计(⾼可⽤),⽬标是⼀个服务注册发现系统,专⻔⽤于微服务的服务发现注册。

Eureka各个节点都是平等的,⼏个节点挂掉不会影响正常节点的⼯作,剩余的节点依然可以提供注册和 查询服务。⽽Eureka的客户端在向某个Eureka注册时如果发现连接失败,会⾃动切换⾄其他节点,只要 有⼀台Eureka还在,就能保证注册服务可⽤(保证可⽤性),只不过查到的信息可能不是最新的(不保 证强⼀致性)

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

如何实现分库分表

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

⽔平:将数据分散到多张表,涉及分区键,

分库:每个库结构⼀样,数据不⼀样,没有交集。库多了可以缓解io和cpu压⼒

分表:每个表结构⼀样,数据不⼀样,没有交集。表数量减少可以提⾼sql执⾏效率、减轻cpu压⼒

垂直:将字段拆分为多张表,需要⼀定的重构

分库:每个库结构、数据都不⼀样,所有库的并集为全量数据

分表:每个表结构、数据不⼀样,⾄少有⼀列交集,⽤于关联数据,所有表的并集为全量数据

存储拆分后如何解决唯⼀主键问题

  • UUID:简单、性能好,没有顺序,没有业务含义,存在泄漏mac地址的⻛险
  • 数据库主键:实现简单,单调递增,具有⼀定的业务可读性,强依赖db、存在性能瓶颈,存在暴露 业务 信息的⻛险
  • redis,mongodb,zk等中间件:增加了系统的复杂度和稳定性
  • 雪花算法

雪花算法原理

image-20241203145227852

主要分为 5 个部分:

  1. 是 1 个 bit:0,这个是无意义的。
  2. 是 41 个 bit:表示的是时间戳。
  3. 是 10 个 bit:表示的是机房 id,0000000000,因为我传进去的就是0。
  4. 是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 0000 0000。

  接下去我们来解释一下四个部分:

1 bit,是无意义的:

  因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

41 bit:表示的是时间戳,单位是毫秒。

  41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。

10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。

  但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),这里可以随意拆分,比如拿出4位标识业务号,其他6位作为机器号。可以随意组合。

12 bit:这个是用来记录同一个毫秒内产生的不同 id。

  12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。也就是同一毫秒内同一台机器所生成的最大ID数量为4096

 简单来说,你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。这个 SnowFlake 算法系统首先肯定是知道自己所在的机器号,(这里姑且讲10bit全部作为工作机器ID)接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。接着用当前时间戳(单位到毫秒)占用41 个 bit,然后接着 10 个 bit 设置机器 id。最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。

代码实现:

  代码中将 10 bit 拆分成 5bit表示工作机器ID,5bit表示数据中心ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
public class SnowflakeIdWorker {

// ==============================Fields===========================================
/** 开始时间戳 (2015-01-01) */
private final long twepoch = 1420041600000L;

/** 机器id所占的位数 */
private final long workerIdBits = 5L;

/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;

/** 序列在id中占的位数 */
private final long sequenceBits = 12L;

/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;

/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;

/** 时间戳向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);

/** 工作机器ID(0~31) */
private long workerId;

/** 数据中心ID(0~31) */
private long datacenterId;

/** 毫秒内序列(0~4095) */
private long sequence = 0L;

/** 上次生成ID的时间戳 */
private long lastTimestamp = -1L;

//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}

// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();

//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}

//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}

//上次生成ID的时间戳
lastTimestamp = timestamp;

//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}

/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间戳
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}

/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}

//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}

SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。但是依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务。

  其实对于分布式ID的生成策略。无论是我们上述提到的哪一种。无非需要具有以下两种特点。 自增的、不重复的。而对于不重复且是自增的,那么我们是很容易想到的是时间,而雪花算法就是基于时间戳。但是毫秒级的并发下如果直接拿来用,显然是不合理的。那么我们就要在这个时间戳上面做一些文章。至于怎么能让这个东西保持唯一且自增。就要打开自己的脑洞了。可以看到雪花算法中是基于 synchronized 锁进行实现的。

如何解决不使⽤分区键的查询问题

  • 映射:将查询条件的字段与分区键进⾏映射,建⼀张单独的表维护(使⽤覆盖索引)或者在缓存中维护
  • 基因法:分区键的后x个bit位由查询字段进⾏hash后占⽤,分区键直接取x个bit位获取分区,查询 字段进⾏hash获取分区,适合⾮分区键查询字段只有⼀个的情况
  • 冗余:查询字段冗余存储

Spring Cloud有哪些常⽤组件,作⽤是什么?

  1. Eureka:注册中⼼
  2. Nacos:注册中⼼、配置中⼼
  3. Consul:注册中⼼、配置中⼼
  4. Spring Cloud Config:配置中⼼
  5. Feign/OpenFeign:RPC调⽤
  6. Kong:服务⽹关
  7. Zuul:服务⽹关
  8. Spring Cloud Gateway:服务⽹关
  9. Ribbon:负载均衡
  10. Spring CLoud Sleuth:链路追踪
  11. Zipkin:链路追踪
  12. Seata:分布式事务
  13. Dubbo:RPC调⽤
  14. Sentinel:服务熔断
  15. Hystrix:服务熔断

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

缓存雪崩是指缓存同⼀时间⼤⾯积的失效,所以,后⾯的请求都会落到数据库上,造成数据库短时间内 承受⼤量请求⽽崩掉。

解决⽅案:

  • 缓存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣。
  • 给每⼀个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓 存。
  • 缓存预热互斥锁

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

解决⽅案:

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

缓存击穿是指缓存中没有但数据库中有的数据(⼀般是缓存时间到期),这时由于并发⽤户特别多,同 时读缓存没读到数据,⼜同时去数据库去取数据,引起数据库压⼒瞬间增⼤,造成过⼤压⼒。和缓存雪 崩不同的是,缓存击穿指并发查同⼀条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从⽽查 数据库。

解决⽅案:

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

分布式系统中常⽤的缓存⽅案有哪些

  • 客户端缓存:⻚⾯和浏览器缓存,APP缓存,H5缓存,localStorage 和 sessionStorage CDN缓 存:内容存储:数据的缓存,内容分发:负载均衡
  • nginx缓存:静态资源
  • 服务端缓存:本地缓存,外部缓存
  • 数据库缓存:持久层缓存(mybatis,hibernate多级缓存),mysql查询缓存 操作系统缓存: PageCache、BufferCache

缓存过期都有哪些策略?

  • 定时过期:每个设置过期时间的key都需要创建⼀个定时器,到过期时间就会⽴即清除。该策略可 以⽴ 即清除过期的数据,对内存很友好;但是会占⽤⼤量的CPU资源去处理过期的数据,从⽽影响 缓存的响应时间和吞吐量
  • 惰性过期:只有当访问⼀个key时,才会判断该key是否已过期,过期则清除。该策略可以最⼤化地 节省CPU资源,但是很消耗内存、许多的过期数据都还存在内存中。极端情况可能出现⼤量的过期 key没有 再次被访问,从⽽不会被清除,占⽤⼤量内存。
  • 定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的expires字典中⼀定数量的key(是随机 的), 并清除其中已过期的key。该策略是定时过期和惰性过期的折中⽅案。通过调整定时扫描的 时间间隔和 每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
  • 分桶策略:定期过期的优化,将过期时间点相近的key放在⼀起,按时间扫描分桶。

常⻅的缓存淘汰算法

  • FIFO(First In First Out,先进先出),根据缓存被存储的时间,离当前最远的数据优先被淘汰;
  • LRU(LeastRecentlyUsed,最近最少使⽤),根据最近被使⽤的时间,离当前最远的数据优先被 淘汰;
  • LFU(LeastFrequentlyUsed,最不经常使⽤),在⼀段时间内,缓存数据被使⽤次数最少的会被 淘汰。

布隆过滤器原理,优缺点

  • 位图:int[10],每个int类型的整数是4*8=32个bit,则int[10]⼀共有320 bit,每个bit⾮0即1,初始 化时都是0
  • 添加数据时:将数据进⾏hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个, 则⼀个数据添加会将多个(hash函数个数)bit改为1,多个hash函数的⽬的是减少hash碰撞的概率
  • 查询数据:hash函数计算得到hash值,对应到bit中,如果有⼀个为0,则说明数据不在bit中,如果 都为1,则该数据可能在bit中

优点:

  • 占⽤内存⼩
  • 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,⼀般⽐较⼩),与数据量⼤⼩⽆关哈 希函数相互之间没有关系,⽅便硬件并⾏运算
  • 布隆过滤器不需要存储元素本身,在某些对保密要求⽐较严格的场合有很⼤优势 数据量很⼤时,布 隆过滤器可以表示全集
  • 使⽤同⼀组散列函数的布隆过滤器可以进⾏交、并、差运算

缺点:

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

分布式缓存寻址算法

  • hash算法:根据key进⾏hash函数运算、结果对分⽚数取模,确定分⽚ 适合固定分⽚数的场景, 扩展分⽚或者减少分⽚时,所有数据都需要重新计算分⽚、存储
  • ⼀致性hash:将整个hash值得区间组织成⼀个闭合的圆环,计算每台服务器的hash值、映射到圆 环中。使⽤相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,找到的第⼀个服务器就 是数据存储的服务器。新增及减少节点时只会影响节点到他逆时针最近的⼀个服务器之间的值 存在 hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决
  • hash slot:将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进⾏hash决定存放 的slot,新增及删除节点时,将slot进⾏迁移即可

什么是Hystrix?简述实现机制

分布式容错框架

  • 阻⽌故障的连锁反应,实现熔断
  • 快速失败,实现优雅降级
  • 提供实时的监控和告警

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

线程隔离:Hystrix会给每⼀个Command分配⼀个单独的线程池,这样在进⾏单个服务调⽤的时 候,就可以在独⽴的线程池⾥⾯进⾏,⽽不会对其他线程池造成影响

信号量隔离:客户端需向依赖服务发起请求时,⾸先要获取⼀个信号量才能真正发起调⽤,由于信 号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进⼊fallback流 程。信号量隔离主要是通过控制并发请求量,防⽌请求线程⼤⾯积阻塞,从⽽达到限流和防⽌雪崩 的⽬的。

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

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

降级:编写好调⽤失败的补救逻辑,然后对服务直接停⽌运⾏,这样这些接⼝就⽆法正常调⽤,但⼜不 ⾄于直接报错,只是服务⽔平下降

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

Spring Cloud和Dubbo有哪些区别?

Spring Cloud是⼀个微服务框架,提供了微服务领域中的很多功能组件,Dubbo⼀开始是⼀个RPC调⽤ 框架,核⼼是解决服务调⽤间的问题,Spring Cloud是⼀个⼤⽽全的框架,Dubbo则更侧重于服务调 ⽤,所以Dubbo所提供的功能没有Spring Cloud全⾯,但是Dubbo的服务调⽤性能⽐Spring Cloud⾼, 不过Spring Cloud和Dubbo并不是对⽴的,是可以结合起来⼀起使⽤的。

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

  1. 当服务A调⽤服务B,服务B调⽤C,此时⼤量请求突然请求服务A,假如服务A本身能抗住这些请 求,但是如果服务C抗不住,导致服务C请求堆积,从⽽服务B请求堆积,从⽽服务A不可⽤,这就 是服务雪崩,解决⽅式就是服务降级和服务熔断。
  2. 服务限流是指在⾼并发请求下,为了保护系统,可以对访问服务的请求进⾏数量上的限制,从⽽防 ⽌系统不被⼤量请求压垮,在秒杀中,限流是⾮常重要的。

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

  1. 服务熔断是指,当服务A调⽤的某个服务B不可⽤时,上游服务A为了保证⾃⼰不受影响,从⽽不再 调⽤服务B,直接返回⼀个结果,减轻服务A和服务B的压⼒,直到服务B恢复。
  2. 服务降级是指,当发现系统压⼒过载时,可以通过关闭某个服务,或限流某个服务来减轻系统压 ⼒,这就是服务降级。

相同点:

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

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

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

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

怎么拆分微服务?

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

  1. 微服务之间尽量不要有业务交叉。
  2. 微服务之前只能通过接⼝进⾏服务调⽤,⽽不能绕过接⼝直接访问对⽅的数据。
  3. ⾼内聚,低耦合。

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

⾼内聚低耦合,是⼀种从上⽽下指导微服务设计的⽅法。实现⾼内聚低耦合的⼯具主要有 同步的接⼝调 ⽤ 和 异步的事件驱动 两种⽅式。

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

什么是DDD: 在2004年,由Eric Evans提出了, DDD是⾯对软件复杂之道。Domain-Driven- Design –Tackling Complexity in the Heart of Software

⼤泥团: 不利于微服务的拆分。⼤泥团结构拆分出来的微服务依然是泥团机构,当服务业务逐渐复杂, 这个泥团⼜会膨胀成为⼤泥团。

DDD只是⼀种⽅法论,没有⼀个稳定的技术框架。DDD要求领域是跟技术⽆关、跟存储⽆关、跟通信⽆ 关。

什么是中台?

所谓中台,就是将各个业务线中可以复⽤的⼀些功能抽取出来,剥离个性,提取共性,形成⼀些可复⽤ 的组件。

⼤体上,中台可以分为三类 业务中台、数据中台和技术中台。⼤数据杀熟-数据中台

中台跟DDD结合: DDD会通过限界上下⽂将系统拆分成⼀个⼀个的领域, ⽽这种限界上下⽂,天⽣就 成了中台之间的逻辑屏障。

DDD在技术与资源调度⽅⾯都能够给中台建设提供不错的指导。

DDD分为战略设计和战术设计。 上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的 指导微服务搭建。

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

  • 开发运维⼀体化。
  • 敏捷开发: ⽬的就是为了提⾼团队的交付效率,快速迭代,快速试错
  • 每个⽉固定发布新版本,以分⽀的形式保存到代码仓库中。快速⼊职。任务⾯板、站⽴会议。团队人员灵活流动,同时形成各个专家代表
  • 测试环境- ⽣产环境 -开发测试环境SIT-集成测试环境-压测环境STR-预投产环境-⽣产环境PRD
  • 晨会、周会、需求拆分会
公告
面试指导,100%帮您拿Offer,不拿Offer不收费!!!