RabbitMQ 消息可靠性投递 + 消费
任何消息中间件发消息投递的可靠性都是开发者选择的重要参考依据。我们希望的是发送的每一条消息都是可以被消费者正确处理的。但是没有哪个消息中间件可以保证消息一定 100% 投递成功,那么如果消息投递失败我们该如何处理呢?
RabbitMQ 消息可靠性理论分析
RabbitMQ消息投递路径
生产者(Producer) --> 交换机(Exchange) --> 队列(Queue) --> 消费者(Consumer)
在这个过程中有三处地方会发生消息丢失可能:
1、Produer 发送消息到 Broker 失败,导致消息发送失败
2、Exchange 投递消息到 Queue 失败,导致消息丢失
3、Consumer 从 Queue 中获取消息,但无法正确处理导致消息丢失
RabbitMQ 消息投递解决方案
1、confirmCallback 处理 生产者发送消息到Broker失败场景,生产者投递消息后,如果Broker收到消息后,会返回生产者一个ACK通知。生产者通过ACK可以确定这条消息是否正常发送到Broker
2、returnCallback:默认情况下交换机投递消息到队列失败是直接丢弃该消息,开启 returnCallback后,如果消息投递失败会通知消息生产者
3、消息确认机制ACK:消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。消费者可以确认消费该消息或者消费失败并放入队列中等待下次继续消费,或者直接拒绝消费该消息
Spring Boot + RabbitMQ 消息可靠性投递实战
confirmCallback实战
开启confirmCallback
#新版,NONE值是禁用发布确认模式,是默认值,CORRELATED值是发布消息成功到交换器后会触发回调方法
spring.rabbitmq.publisher-confirm-type: correlated
代码开发
/**
* 生产者投递消息后,如果Broker收到消息后,会给生产者一个ACK。生产者通过ACK,可以确认这条消息是否正常发送到Broker,这种方式是消息可靠性投递的核心
* 步骤1:yaml文件中添加配置 spring.rabbitmq.publisher-confirm-type: correlated
* 步骤2:编写代码
*/
@PostConstruct
public void setConfirmCallback() {
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 发送消息时指定的唯一关联数据(消息id)
* @param ack 投递结果
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息投递到交换机成功:[correlationData={}]", correlationData);
} else {
log.error("消息投递到交换机成功:[correlationData={},原因={}]", correlationData, cause);
}
}
});
}
returnCallback实战
开启returnCallback配置
spring.rabbitmq.publisher-returns=true
修改交换机投递到队列失败的策略
#为true,则交换机处理消息到路由失败,则会返回给生产者
spring.rabbitmq.template.mandatory=true
代码实战
/**
*
* 注意下面两项必须同时配置
* # 开启阶段二(消息从E->Q)的确认回调 Exchange --> Queue returnCallback
* spring.rabbitmq.publisher-returns=true
*
* #为true,则交换机处理消息到路由失败,则会返回给生产者
* spring.rabbitmq.template.mandatory=true
*/
@PostConstruct
public void setQueueCallback() {
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error("路由到队列失败,[消息内容:{},交换机:{},路由件:{},回复码:{},回复文本:{}]",
returnedMessage.getMessage(), returnedMessage.getExchange(),
returnedMessage.getRoutingKey(), returnedMessage.getReplyCode(), returnedMessage.getReplyText());
}
});
}
消息确机制ACK实战
消息确认机制ACK介绍
- 消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除
- 消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中
- 只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。
- 消息的ACK确认机制默认是打开的,消息如未被进行ACK的消息确认机制,这条消息被锁定Unacked
开启手动确认消费配置
#开启手动确认消息,如果消息重新入队,进行重试
spring.rabbitmq.listener.simple.acknowledge-mode: manual
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled=true
代码实战
@RabbitHandler
public void releaseOrder(String orderSn, Message message, Channel channel) {
long msgTag = message.getMessageProperties().getDeliveryTag();
log.info("获取到消息,msgTag={},message={},body={}", msgTag, message.toString(), orderSn);
try {
// 业务处理
channel.basicAck(msgTag, false);
} catch (Exception e) {
log.error("关闭订单失败,orderSn={}", orderSn);
}
}
RabbitMQ中消费者有3种签收消息方式
- channel.basicAck(long deliveryTag, boolean multiple):确认签收消息
- channel.basicNack(long deliveryTag, boolean multiple, boolean requeue):表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
- channel.basicReject(long deliveryTag, boolean requeue):拒绝消息,与
basicNack
区别在于不能进行批量操作,其他用法很相似
前面提到,由于网络等一些原因引起了故障,就会导致消息被重发。因此,消费端一定要做好处理重复消息的准备,强烈建议在消费端实现「幂等」的业务逻辑。
在消费端还有一种情况,就是当前消费者认为它不能处理当前消息。因此,它就拒绝签收(basic.reject或者basic.nack)这个消息。那么生产者也需要监听这些消息并做特殊的业务处理。
遗留问题
问题1:生产者发送消息到Broker失败后,如何处理消息?
问题2:消费者消费消息失败,放入队列中重试,如何控制重试次数,多次重试后依然错误如何处理?