消息可靠性投递:
一、问题分析:
消息从生产者发送到exchange,再到queue,再到消费者,有哪些导致消息丢失的可能性?
1.1 发送时丢失:
a. 生产者发送的消息未送达exchange
b. 消息到达exchange后未到达queue
1.2 MQ宕机;
1.3 consumer接收到消息后未消费就宕机
二、问题解决:
1、生产者消息确认
1.1 理论:
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求:
- publisher-confirm,发送者确认:消息成功投递到交换机,返回ack;消息未投递到交换机,返回nack
- publisher-return,发送者回执:消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
注:消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
1.2 实现:
- 在publisher这个微服务的application.yml中添加配置:
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
消息确认机制有两种:
simple:生产者同步等待confirm结果,直到超时(不推荐)
correlated:生产者异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback(推荐),需要给消息设置UUID;
publish-returns:
开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback(它是全局的,需要在项目启动的时候加载)
template.mandatory:
true,则调用ReturnCallback;
false:则直接丢弃消息
- 每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置:
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果需要,可以重新发送消息。
});
}
}
- 发送消息,指定消息ID、消息ConfirmCallback
消息成功发送到exchange,返回ack
消息发送失败,没有到达交换机,返回nack
消息发送过程中出现异常,没有收到回执
@Test
public void testSendMessage2SimpleQueue() throws InterruptedException {
// 消息体
String message = "hello, spring amqp!";
// 消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 添加callback
correlationData.getFuture().addCallback(
result -> {
if (result.isAck()) {
// ack,消息成功
log.debug("消息发送成功, ID:{}", correlationData.getId());
} else {
// nack,消息失败
log.error("消息发送失败, ID:{}, 原因{}", correlationData.getId(), result.getReason());
}
},
ex -> log.error("消息发送异常, ID:{}, 原因{}", correlationData.getId(), ex.getMessage()));
// 发送消息。由于需要做消息确认机制,所以要添加参数correlationData
rabbitTemplate.convertAndSend("amq.direct", "simple", message, correlationData);
}
2、消息持久化
2.1 理论:
即使保证了消息可靠地发到了MQ中,但MQ的消息存储在内存中,一旦宕机,也一样还是会丢失消息。 所以为了解决这个问题,我们应该把消息持久化到磁盘中。
2.2 实现:
MQ默认是内存存储消息,开启持久化功能可以确保缓存在MQ中的消息不丢失。
- 交换机持久化:
@Bean
public DirectExchange simpleExchange(){
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new DirectExchange("simple.direct", true, false); }
}
- 队列持久化:
@Bean
public Queue simpleQueue(){
// 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();
}
- 消息持久化,SpringAMQP中的消息默认是持久的,可以通过MessageProperties中的DeliveryMode来指定的:
Message msg = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8)) // 消息体
.setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 持久化
.build();
3、消费者消息确认
3.1 理论:
RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。而SpringAMQP则允许配置三种确认模式:
- manual:手动ack,需要在业务代码结束后,调用api发送ack。
- auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
- none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
4、消费失败重试机制
4.1 理论:
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力;
我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是交给MQ进行无限制的requeue到mq队列。当重试打到最大次数后仍然不成功,则将这条消息丢弃掉。
关于消息被丢弃,新的策略如下:
消息失败重试策略,是根据MessageRecoverer接口来处理的,它包含了三种不同的实现:
a. RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
b. ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
c. RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
4.2 实现:
- 首先,定义接收失败消息的交换机、队列及其绑定关系:
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(){
return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
}
- 然后,定义RepublishMessageRecoverer的Bean覆盖SpringBoot的默认配置:
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}