面试题四
1、限流什么是令牌桶算法?什么是时间窗口滑动算法?
计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter
法虽然简单,但是有一个十分致命的问题,那就是临界问题,我们看下图:
在这里插入图片描述
从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,这种情况,其实是符合我们上述规则的。因为在0:00-0:59这个区间用户确实没有超过我们设置的100这个最大范围,1:00-1:59这个区间也是一样。
但是,其实这个用户在 0:59-1:00这1秒里,瞬间发送了200个请求,这种情况使用固定窗口的计数器就很明显不符合我们的初衷。
在这里插入图片描述
在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口 划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1
漏桶算法(Leaky Bucket)
漏桶算法由流量容器、流量入口和出口组成。其中流量出口流速即为我们期望的限速值,比如 100 QPS。漏桶算法除了具备限流能力,还具备流量整型功能。下面我们通过一张图来了解漏桶算法。
在这里插入图片描述
如上图,流入漏桶流量的流速是不恒定的,经过漏桶限速后,流出流量的速度是恒定的。需要说明的是,漏桶的容量是有限的,一旦流入流量超出漏桶容量,这部分流量只能被丢弃了
缺点
如图所示,不管流量多大,超出的部分都会直接丢弃,即使服务器还有大量空闲资源也是直接丢弃,无法处理突发流量。那么如何解决这个问题呢?下面介绍令牌桶算法。
令牌桶算法(Token Bucket)
它的运行过程是这样的,一个令牌工厂按照设定值定期向令牌桶发放令牌。当令牌桶满了后,多出的令牌会被丢弃掉。每当一个请求到来时,该请求对应的线程会从令牌桶中取令牌。如果遇到突发情况,初期由于令牌桶中存放了很多个令牌,因此允许多个请求同时取令牌。当桶中没有令牌后,无法获取到令牌的请求可以丢弃,或者重试。
2、dubbo和openfeign的区别?
一、相同点
Dubbo 与 Feign 都依赖注册中心、负载均衡。
二、区别
1、协议
Dubbo:
1 | |
Feign:
基于Http传输协议,短连接,不适合高并发的访问。
2、负载均衡
Dubbo:
1 | |
Feign:
1 | |
3、容错策略
Dubbo:
支持多种容错策略:failover、failfast、brodecast、forking等,也引入了retry次数、timeout等配置参数。
Feign:
利用熔断机制来实现容错的,处理的方式不一样
3、sentinel和hystrix的区别?
2. 功能对比:
| 功能 | Sentinel | Hystrix | resilience4j |
|---|---|---|---|
| 隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
| 熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
| 实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
| 动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
| 扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
| 基于注解的支持 | 支持 | 支持 | 支持 |
| 限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
| 流量整形 | 支持预热模式、匀速器模式、预热排队模式(流量规则处可配置) | 不支持 | 简单的 Rate Limiter 模式 |
| 系统自适应保护 | 支持 | 不支持 | 不支持 |
| 控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
4、RabbitMQ的交换机有哪些?
分别是Direct Exchange(直连交换机), Fanout Exchange(扇型交换机), Topic Exchange(主题交换机)与 Headers Exchange(头交换机)
Direct Exchange(直连交换机):将一个名为Q的消息队列与某个名为D的直连交换机通过值为R的路由键绑定在一起,当一个Msg和路由键R发送到直连交换机D上时,直连交换机D会把Msg根据路由键R分发到Q队列。这种模式类似于一对一
Fanout Exchange(扇型交换机):当一个Msg发送到扇形交换机F上时,则扇形交换机F会将消息分别发送给所有绑定到F上的消息队列。扇形交换机将消息路由给绑定到自身的所有消息队列,也就是说路由键在扇形交换机里没有作用,故消息队列绑定扇形交换机时,路由键可为空。这个模式类似于广播。
Topic Exchange(主题交换机):主题交换机是一种发布/订阅的模式,结合了直连交换机与扇形交换机的特点,消息队列与主题交换机的绑定也是通过路由键的。当一个Msg和路由键规则发送到一个主题交换机T时,T会根据路由键规则来筛选出符合规则的绑定到自身消息队列的路由键(可能是1个,也可能是N个,也可能是0个),根据符合的路由键,将消息发送到其对应的消息队列里。这个模式类似于多播,当消息的路由规则只匹配到一个路由键时,此时主题交换机可以看作是直连交换机,当路由规则匹配了主题交换机上所有绑定的队列的路由键时,此时主题交换机可以看作是扇形交换机
Headers Exchange(头交换机):头交换机类似与主题交换机,但是却和主题交换机有着很大的不同。主题交换机使用路由键来进行消息的路由,而头交换机使用消息属性来进行消息的分发,通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。在头交换机里有一个特别的参数”x-match”,当”x-match”的值为“any”时,只需要消息头的任意一个值匹配成功即可,
当”x-match”值为“all”时,要求消息头的所有值都需相等才可匹配成功。
5、RabbitMQ消息的可靠性如何保证的?
1、保证消息不丢失(三步)
1 | |
2、保证消息不重复消费
幂等性 ( 每个消息用一个唯一标识来区分,消费前先判断标识有没有被消费过,若已消费过,则直接 ACK)
3、RabbitMQ如何保证消息的顺序性
将消息放入同一个交换机,交给同一个队列,这个队列只有一个消费者,消费者只允许同时开启一个线程
4、RabbitMQ消息重试机制
消费者在消费消息的时候,如果消费者业务逻辑出现程序异常,这时候应该如何处理?
使用消息重试机制 (SpringBoot 默认 3 次消息重试机制 )
如何合适选择重试机制?
消费者取到消息后,调用第三方接口,接口无法访问,需要使用重试机制
消费者取到消息后,抛出数据转换异常,不需要重试机制,需要发布者进行解决。
5、SpringBoot消息重试机制
@EnableRetry 注解:表示启用重试机制(value 表示哪些异常需要触发重试, maxAttempts 设置最大重试次数,delay 表示重试的延迟时间, multiplier 表示上一次延时时间是这一次的倍数 )
eg 、 @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000,
multiplier = 1.5))
@Recover 注解:当重试次数达到设置的最大次数的时候,程序还是执行异常,调用的回调函数。
6、RabbitMQ死信队列
死信队列是当消息在一个队列因为下列原因 :
a 、消息被拒绝 (basic.reject 或 basic.nack) 并且 requeue=false.
b 、消息 TTL 过期
c 、队列达到最大长度 ( 队列满了,数据无法添加到 mq 中 )
变成了 “ 死信队列 ” 后被重新投递 (publish) 到另一个 Exchange ,然后重新消费。说白了就是没有被消费 的消息换个地方重新被消费
7、RabbitMQ解决分布式事务
经典案例,以目前流行的外卖为例,用户下单后,调用订单服务,订单服务调用派单系统通知送外卖人 员送单,这时候订单系统与派单系统采用MQ 异步通讯。
RabbitMQ 解决分布式事务原理
答案:采用最终一致性原理 需要保证以下三要素 :
a 、确保生产者一定要将数据投递到 MQ 服务器中 ( 采用 MQ 消息确认机制 )
b 、确保消费者能够正确消费消息,采用手动 ACK 模式 ( 注意重试、幂等性问题 )
c 、如何保证第一个事务先执行,采用补偿机制,在创建一个补单消费者进行监听,如果订单没有创建成 功,进行补单。( 如果第一个事务中出错,补单消费者会在重新执行一次第一个事务,例如第一个事务是 添加订单表,如果失败在补单的时候重新生成订单记录,由于订单号唯一,所以不会重复)
8、RabbitMQ保证消息不丢失的具体方案
前提 :
1 | |
涉及到的技术点:
1 | |
8.1、保证投放消息不丢失
(1)先将消息放入生产者Redis(此时消息的状态为未投放),再放入队列
(2)根据confirm(ReturnCallback和ConfirmCallback)的结果来确定消息是否投递成功,
投递成功的,修改生产者redis中消息的投递状态为已投递
投递失败的消息将会放入失败的Redis,并从生产者Redis中删除,由定时任务定期扫描并重新投递
(3)生产者Redis定时任务
生产者Redis定时任务专门扫描生产者Redis中存放了一定时间,但是状态还是未投放的消息
此消息会被认为已经投递,但是没有任何反馈结果(由于不可知因素,导致没有ReturnCallback,也没有 ConfirmCallback),
此类消息被扫描到后,会放入失败的Redis,并从生产者Redis中删除,由定时任务定期扫描并重新投递
(4)还需要一个专门的定时任务扫描生产者Redis中存放了很久,仍然未消费的数据(状态为已投递),此类 消息被扫描到后,会放入失败的Redis,并从生产者Redis中删除,由定时任务定期扫描并重新投递
(5)扫描失败的Redis的定时任务都遵循一条原则,一条消息最多被重新投递三次,若投递了三次仍然失 败,则记录日志,记录到数据库,不会再投递,需要人工干预处理
8.2、保证消费消息不丢失
(1)消费者取到消息后,从消息中取出唯一标识,先判断此消息有没有被消费过,若已消费过,则直接 ACK(避免重复消费)
(2)正常处理成功后,将生产者Redis中的此消息删除,并ACK(告诉server端此消息已成功消费)
(3)遇到异常时,捕获异常,验证自己在消息中设定的重试次数是否超过阀值,若超过,则放入死信队 列,若未超过,则向将消息中的重试次数加1,抛出自定义异常,进入重试机制
(4)有专门的消费者用于处理死信队列中消费多次仍未消费成功的数据,可以记录日志,入库,人工干预 处理
