1 常见问题
- 消息可靠性问题
- 延迟消息问题
- 消息堆积问题
- 高可用问题
2 消息可靠性
2.1 消息可靠性问题
消息从生产者发送到exchange,再到queue,再到消费者,有哪些导致消息丢失的可能性?
- 发送时丢失
-
- 生产者发送的消息未送达exchange
-
- 消息到达exchange后未到达queue
- MQ宕机,queue将消息丢失
- consumer接受到消息未消费宕机
2.2 生产者消息确认
2.2.1 生产者确认机制
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息到达MQ后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种请求:
- publisher-confirm,发送者确认
消息成功投递到交换机,返回ack
消息未投递到交换机,返回nack - publisher-return,发送者回执
消息投递到交换机了,但是没有路由到队列。返回ack,及路由失败原因
2.2.2 实现生产者确认
1 在 publisher 的 yaml 中添加配置
# 开启 publisher-confirm
# simple: 同步等待回调
# correlated: 异步回调,定义ConfirmCallback,MQ返回结果会回调这个ConfirmCallback
publisher-confirm-type: correlated
# 开启 publisher-return 功能,同样是基于callback机制,不过是定义ReturnCallback
publisher-returns: true
# 定义消息路由失败时的策略。true: 则回调ReturnCallback; false: 则直接丢弃消息
template:
mandatory: true
2 ReturnCallback
每个RabbitTemplate 只能配置一个ReturnCallback(全局共用)
@Configuration
@Slf4j
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> {
log.info("ReturnCallback...");
log.info("消息: {}", message);
log.info("应答码: {}", i);
log.info("失败原因: {}", s);
log.info("路由key: {}", s1);
log.info("消息: {}", s2);
});
}
}
3 ConfirmCallback
// 消息体
String message = "hello rabbitmq";
// 消息id 消息配上唯一的id,为了区分消息
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 添加 callback
correlationData.getFuture().addCallback(confirm -> {
// 成功回调
if (confirm.isAck()) {
// ack
log.info("消息成功投递到交换机! {}", correlationData.getId());
} else {
// nack
log.info("消息投递到交换机失败! {}", correlationData.getId());
}
}, throwable -> {
// 失败回调
log.info("消息发送失败: {}", throwable.getMessage());
});
// 发送消息
rabbitTemplate.convertAndSend("amq.exchange", "simple", message, correlationData);
2.3 消息持久化
MQ是默认是内存存储消息,开启持久化功能可以确保缓存在MQ中的消息不丢失
2.3.1 交换机持久化
@Bean
public DirectExchange durableExchange() {
// 三个参数: 交换机名称,是否持久化,当没有queue与其绑定时自动删除
return new DirectExchange("durable.exchange", true, false);
}
2.3.2 队列持久化
@Bean
public Queue simpleQueue() {
return QueueBuilder.durable("durable.queue").build();
}
2.3.3 消息持久化
Message msg = MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久化
.build();
2.4 消费者消息确认
RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。
而SpringAMQP允许配置三种确认方式
- manual: 手动ack,需要在业务代码结束后,调用api发送ack
- auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
- none: 关闭ack,MQ假定消费者获取消息必定成功处理,投递消息后立马删除
2.4.1 yaml配置
spring:
rabbitmq:
listener:
simple:
prefetch: 1
acknowledge-mode: auto
2.5 消费失败重试机制
当消费者出现异常后,消息会不断 requeue (重新入队) 到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息飙升,带来不必要的压力
2.5.1 spring retry机制 (重试模式)
spring:
rabbitmq:
listener:
simple:
prefetch: 1
retry:
# 开启消费者重试
enabled: true
# 初次失败等待时长为1秒
initial-interval: 1000
# 下次失败的等待时长倍数,下次等待时长 = multiplier * initial-interval
multiplier: 1
# 最大重试次数
max-attempts: 3
# true无状态;false有状态。如果业务中包含事务,这里改为false
stateless: true
2.5.2 MessageRecover
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要MessageRecover接口来处理,它包含三种不同的实现
- RejectAndDontRequeueRecover: 重试失败后,直接reject,丢弃消息(默认)
- ImmediateRequeueMessageRecover: 重试失败后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
2.5.3 RepublishMessageRecoverer模式
1 定义失败消息的交换机、队列及其绑定关系
@Bean
public DirectExchange errorExchange() {
return new DirectExchange("error.exchange");
}
@Bean
public Queue errorQueue() {
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(){
return BindingBuilder.bind(errorQueue()).to(errorExchange()).with("error");
}
2 定义RepublishMessageRecoverer
@Bean
public RepublishMessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.exchange", "error");
}
3 延迟消息问题
3.1 死信交换机
3.1.1 死信
当一个队列中的消息满足下列情况之一时,可以成为死信
- 消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息堆积满了,最早的消息可能成为死信
3.1.2 死信交换机
如果一个队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)
3.2 TTL
TTL,也就是Time-To-Live。如果一个队列中的消息TTL结束仍未消费,则会变成死信,ttl超时会分为两种情况:
- 消息所在的队列设置了存活时间
- 消息本身设置了存活时间
接收方
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.queue", durable = "true"),
exchange = @Exchange(name="dl.direct"),
key = "dl"
))
public void listenDlQueue(String msg){
log.info("接收到 dl。queue的延迟消息:{}", msg);
}
发送方:
@Bean
public DirectExchange ttlExchange() {
return new DirectExchange("ttl.direct");
}
@Bean
public Queue ttlQueue() {
return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
.ttl(10000) // 设置队列的超时时间, 10秒
.deadLetterExchange("dl.direct") // 指定死信交换机
.deadLetterRoutingKey("dl") // 指定死信 RoutingKey
.build();
}
@Bean
public Binding ttlBinding() {
return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}
RabbitTemplate rabbitTemplate = run.getBean(RabbitTemplate.class);
Message msg = MessageBuilder.withBody("ttl message".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT) //持久化
.setExpiration("5000") // 消息失效时间
.build();
rabbitTemplate.convertAndSend("ttl.direct","ttl", msg);
3.3 延迟队列
利用 TTL 结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式被称为延迟队列(Delay Queue)模式。
延迟丢列的使用场景包括:
- 延迟发送短信
- 用户下单,如果用户15分钟内未支付,则自动取消
- 预约工作会议,20分钟后自动通知所有参会人员