消息从发送,到消费者接收,会经历多个过程:
其中的每一步都可能导致消息丢失,常见的丢失原因包括:
- 发送时丢失:
- 生产者发送的消息未送达exchange
- 消息到达exchange后未到达queue
- MQ宕机,queue将消息丢失
- consumer接收到消息后未消费就宕机
针对这些问题,RabbitMQ分别给出了解决方案。也就是面试题常问的如何rabbitmq如何保证消息的可靠性?
- 生产者确认机制
- mq持久化
- 消费者确认机制
- 失败重试机制
生产者确认机制(解决生产者->交换机->队列)
步骤
1.修改生产者配置
java
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
publish-confirm-type
:开启publisher-confirm,这里支持两种类型:simple
:同步等待confirm结果,直到超时correlated
:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallbackpublish-returns
:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallbacktemplate.mandatory
:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
2.定义回调函数(每个RabbitTemplate只能配置一个ReturnCallback)
消息从交换机到队列的过程不通就会回调这个函数
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
@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());
// 如果有业务需要,可以重发消息
});
}
}
3.定义ConfirmCallback 消息每次发送前都要写
java
public void testSendMessage2SimpleQueue() throws InterruptedException {
// 1.消息体
String message = "hello, spring amqp!";
// 2.全局唯一的消息ID,需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 3.添加callback
correlationData.getFuture().addCallback(
result -> {
if(result.isAck()){
// 3.1.ack,消息成功
log.debug("消息发送成功, ID:{}", correlationData.getId());
}else{
// 3.2.nack,消息失败
log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
}
},
ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
);
// 4.发送消息
rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);
// 休眠一会儿,等待ack回执
Thread.sleep(2000);
}
mq持久化(推荐惰性队列)
-
交换机持久化
-
队列持久化
-
消息持久化
-
交换机持久化
java
@Bean
public DirectExchange simpleExchange(){
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new DirectExchange("simple.direct", true, false);
}
- 队列持久化
java
@Bean
public Queue simpleQueue(){
// 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();
}
消息持久化
发送时,可以设置消息的属性(MessageProperties),指定delivery-mode
但是这三种都不是很好的方式
惰性队列:从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
消费者确认机制
直接配置即可
java
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 关闭ac
- none模式下,消息投递是不可靠的,可能丢失
- auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
- manual:自己根据业务情况,判断什么时候该ack
一般,我们都是使用默认的auto即可。
手动ack
java
@RabbitListener(queues = "fanout.queue1")
public void onMessage(Message message, Channel channel) throws Exception {
try {
// 处理消息
processMessage(message);
// 手动确认消息已经被处理完成
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 发生异常时可以选择拒绝消息,重新放回队列或者进入死信队列等
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
失败重试机制
1.生产者发送消息失败配置
默认是消息重新回到队列,但是如果一直失败,就会一直循环的失败回到队列再重试,频率太高太消耗资源了
可以本地重试也就是重写发送消息
java
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
经典面试题:如何保证消息不会重复消费呢?
1.消费端手动ACK确认机制
在消费端使用RabbitMQ提供的手动ACK确认机制,在消费者成功处理消息后,手动将消息从队列中删除。这样可以确保消息只会被处理一次,避免了重复消费的问题。
2.消费端去重保证机制
可以在消费端处理每条消息之前,通过分布式锁或者数据库唯一索引等方式,判断当前消息是否已经被处理过。如果已经处理过,则忽略该消息;否则正常处理,并将消息标记为已处理。
死信队列
当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):
- 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息满了,无法投递
如果这个包含死信的队列配置了dead-letter-exchange
属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,检查DLX)。
另外,队列将死信投递给死信交换机时,必须知道两个信息:
- 死信交换机名称
- 死信交换机与死信队列绑定的RoutingKey
这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列。
如
java
// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2(){
return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
.deadLetterExchange("dl.direct") // 指定死信交换机
.build();
}
// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange(){
return new DirectExchange("dl.direct", true, false);
}
// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue(){
return new Queue("dl.queue", true);
}
// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding(){
return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
}
死信交换机的使用场景是什么?
- 如果队列绑定了死信交换机,死信会投递到死信交换机;
- 可以利用死信交换机收集所有消费者处理失败的消息(死信),交由人工处理,进一步提高消息队列的可靠性。
延迟队列
概念:延迟队列是一种消息中间件中的特殊队列,用于暂存需要延迟处理的消息,并在一定时间后重新发送到原来的队列或其他队列中进行处理
延迟消息 :是指具有延迟时间属性的消息,在发送时可以设置消息的延迟时间。这个延迟时间表示消息需要在多少时间后才能被消费者消费。
两者关系:延迟队列是实现延迟消息的一种方式
定时消息和延迟消息本质上是相同的。它们都根据消息设置的计时时间在固定时间向消费者传递消息。 -----rocketmq官网写的
允许消息在设置的时间内未被消费,如果到期,未消费发送到死信队列
模式一 :消息本身就可以设置过期时间,设置了过期时间的消息发送到一个没有消费者监听的队列,该队列绑定了死信交换机。当消息过期就会发送到死信交换机,然后再到死信队列。我们只需监听死信队列即可
上面这种方式不推荐。
官方推荐
模式二使用插件,
RabbitMQ有一个官方的插件社区,地址为:https://www.rabbitmq.com/community-plugins.html
其中包含各种各样的插件,包括我们要使用的DelayExchange插件:
大家可以去对应的GitHub页面下载3.8.9版本的插件,地址为https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9这个对应RabbitMQ的3.8.5以上版本