目录
1. 问题引入
在前面初识RabbitMq-CSDN博客我们介绍了Rabbit的三个优点
- 异步提速
- 应用解耦
- 削峰填谷
那么使用RabbitMq又有什么问题呢?
还是用以下支付服务的流程来说明
正常的业务流程是:用户服务的扣减余额完成了,然后MQ就开始通知各服务,各服务完成自己的任务。
但是现在MQ通知订单服务失败了(可能是MQ宕机了,或者网络波动)这就导致了余额扣减了,但是订单消息却没有改变。那用户肯定是不答应的,因此在使用RabbitMQ时就必须保证消息的可靠性 即:消息应该至少被消费者处理1次
2. 如何保证消息的可靠性
我们可以从Rabbit的架构来分析 Pulisher -> MQ -> consumer
2.1 发布者的可靠性
2.1.1 发布消息时可能遇到的问题
- MQ宕机无法连接
- 消息到达MQ但Exchange不存在
- 消息到达MQ并被Exchange转发当RoutingKey错误 不能到达Queue中
- 消息到达MQ,处理消息的进程发生异常(发送网络波动)
2.1.2 解决方案
生产者重试机制
SpringAMQP中为我们提供消息发送的重试机制。即:当RabbitTemplate与MQ连接超时后,多次重试
我们在Pulisher模块中修改yml配置文件即可开启重试机制
Spring:
rabbitmq:
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms #失败后的初始等待时间
multiplier: 1 #失败下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 #最大重试数
我们此时停掉RabbitMQ服务然后启动Pulisher
可以看到超时了3次每次等待一秒。
注意:当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的。 如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
生产者确认机制
一般来说网络通畅的话,基本不会发送消息丢失的问题的。当然也有可能因为一些编程时的原因导致
1.无法找到Exchange
2.RoutingKey错误无法找到合适的Queue
为了解决这种问题SpringAMQP提供了生产者确认机制 他包括两种类型publisher-confirm和publisher-returns。
在Spring的配置文件中添加以下配置即可开启
spring:
rabbitmq:
publisher-confirm-type: correlated # none关闭确认 simple同步阻塞等待mq的回执 correlated mq异步回调放回回执
publisher-returns: true # 开启return机制
在开启确认机制的情况下,当生产者发送消息给MQ后,MQ会根据消息处理的情况返回不同的回执。
-
当消息投递到MQ,但是路由失败时,通过Publisher Return返回异常信息,同时返回ack的确认信息,代表投递成功
-
临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
-
持久消息投递到了MQ,并且入队完成持久化,返回ACK ,告知投递成功
-
其它情况都会返回NACK,告知投递失败
实现生产者确认机制
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@AllArgsConstructor
@Slf4j
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("触发return callback");
log.debug("exchange{}",returnedMessage.getExchange());
log.debug("msg{}",returnedMessage.getMessage());
}
});
}
}
@Test
void testPublisherConfirm() {
//1.创建CorrelationData
CorrelationData correlationData = new CorrelationData();
//2. 给Future添加ConfirmCallback
correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
//Future发生异常时的处理逻辑,基本不会触发
log.error("send message fail",ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
//Future接受到回执的处理逻辑,参数中的result就是回执内容
if (result.isAck()){ // true 代表ack回执 ,false代表nack回执
log.debug("收到ack回执");
}else{
log.error("发送消息失败,收到nack reason:{}",result.getReason());
}
}
});
rabbitTemplate.convertAndSend("lx.direct","red","hello",correlationData);
}
开启生产者确认模式会导致运行速度的下降,并且产生这种错误大多是由于编程的错误产生的,所以一般是不开启的。
2.2 MQ的可靠性
消息到到MQ也有可能会出现问题,为了提升性能,默认情况下MQ的数据都是在内存存储的临时数据,重启后就会消失。所以我们要保证消息的持久化
1. 交换机的持久化
2. 队列的持久化
3. 消息的持久化
这样我们就能保证消息不会因为宕机或者队列,交换机消失而丢失了。
2.3 消费者的可靠性
2.3.1 消息达到消息者时可能出现的问题
1. 消费者处理消息时发生异常。
2. 网络发生波动,消息被重复投递。
3. 消费者处理消息时服务突然宕机。
当发生上述问题时,消息就会丢失。这时RabbitMQ就需要知道consumer的处理状态,也就是需要一个回执。
2.3.2 消费者确认模式
SpringAMQP为我们提供了消息者确认机制(Consumer Acknowledgement)。即:当消费者处理消息结束后,应该向RabbitMQ发送一个回执,告知RabbitMQ自己消息处理状态。回执有三种可选值:
-
ack:成功处理消息,RabbitMQ从队列中删除该消息
-
nack:消息处理失败,RabbitMQ需要再次投递消息
-
reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
一般reject方式用的较少,除非是消息格式有问题,那就是开发问题了。因此大多数情况下我们需要将消息处理的代码通过try catch
机制捕获,消息处理成功时返回ack,处理失败时返回nack.
由于消息回执的处理代码比较统一,因此SpringAMQP帮我们实现了消息确认。并允许我们通过配置文件设置ACK处理方式,有三种模式:
-
**none**
:不处理。即消息投递给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用 -
**manual**
:手动模式。需要自己在业务代码中调用api,发送ack
或reject
,存在业务入侵,但更灵活 -
**auto**
:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack
. 当业务出现异常时,根据异常判断返回不同结果:-
如果是业务异常,会自动返回
nack
; -
如果是消息处理或校验异常,自动返回
reject
;
-
添加以下配置开启消费者确认模式
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 自动ack
这样当消息处理失败时就会重新入队
2.3.2 消费者重试模式
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。 极端情况就是消费者一直无法执行成功,那么消息requeue就会无限循环,导致mq的消息处理飙升,带来不必要的压力
为了应对上述情况,SpringAMQP又提供了消费者重试机制
添加以下配置开启
spring:
rabbitmq:
listener:
retry:
enabled: true
initial-interval: 1000ms
multiplier: 3
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
2.3.3 失败处理策略
在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,显然不太合适了。 因此Spring允许我们自定义重试次数耗尽后的消息处理策略,这个策略是由MessageRecovery
接口来定义的,它有3个不同实现:
-
RejectAndDontRequeueRecoverer
:重试耗尽后,直接reject
,丢弃消息。默认就是这种方式 -
ImmediateRequeueMessageRecoverer
:重试耗尽后,返回nack
,消息重新入队 -
RepublishMessageRecoverer
:重试耗尽后,将失败消息投递到指定的交换机
比较优雅的一种处理方案是RepublishMessageRecoverer
,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue() {
return new Queue("error.queue");
}
@Bean
public Binding errorBinding(){
return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
}
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
}
}
2.3.4 业务幂等性
幂等性指的是同一个业务执行一次和执行多次对业务的状态的改变是一样的。
在前面我们提到了一个问题网络波动问题可能导致消息会被重复投递。为了解决这个问题,我们就需要给消息添加一个全局唯一id。
和之前消息转换器一样这里对消息添加一个id就可以了。
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jjmc.setCreateMessageIds(true);
return jjmc;
}