上一篇文章里,咱们了解了如何保证消息被可靠投递到RabbitMQ的交换机中,但还有一些不完美的地方,试想一下,若是向RabbitMQ服务器发送一条消息,服务器确实也接收到了这条消息,因而给你返回了ACK确认消息,但服务器拿到这条消息一看,找不到路由它的队列,因而就把它丢进了垃圾桶,emmm,我猜应该属于可回收垃圾。java
若是你对上面的描述还不是很清楚,那我再用代码来讲明一次。服务器
在仅开启了生产者确认机制的状况下,交换机接收到消息后,会直接给消息生产者发送确认消息,若是发现该消息不可路由,那么消息会被直接丢弃,此时,生产者是不知道消息被丢弃这个事件的。dom
咱们将上一篇中的交换机类型改成DirectExchange,这样就只有当消息的 RoutingKey 和队列绑定时设置的 Bindingkey (这里即“key”)一致时,才会真正将该消息进行路由。ide
public static final String BUSINESS_EXCHANGE_NAME = "rabbitmq.tx.demo.simple.business.exchange";
public static final String BUSINESS_QUEUEA_NAME = "rabbitmq.tx.demo.simple.business.queue";
// 声明业务 Exchange
@Bean("businessExchange")
public DirectExchange businessExchange(){
return new DirectExchange(BUSINESS_EXCHANGE_NAME);
}
// 声明业务队列
@Bean("businessQueue")
public Queue businessQueue(){
return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).build();
}
// 声明业务队列绑定关系
@Bean
public Binding businessBinding(@Qualifier("businessQueue") Queue queue, @Qualifier("businessExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key");
}
复制代码
对消息生产者也稍做修改:函数
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
private void init() {
// rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setConfirmCallback(this);
}
public void sendCustomMsg(String exchange, String msg) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key", msg, correlationData);
correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key2", msg, correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData != null ? correlationData.getId() : "";
if (b) {
log.info("消息确认成功, id:{}", id);
} else {
log.error("消息未成功投递, id:{}, cause:{}", id, s);
}
}
复制代码
而后咱们调用该方法,发送两条消息测试一下:学习
消息id:ba6bf502-9381-4220-8dc9-313d6a289a4e, msg:1
消息id:f0040a41-dc02-4e45-b8af-e3cfa8a118b2, msg:1
消息确认成功, id:ba6bf502-9381-4220-8dc9-313d6a289a4e
消息确认成功, id:f0040a41-dc02-4e45-b8af-e3cfa8a118b2
收到业务消息:1
复制代码
能够看到,发送了两条消息,第一条消息的 RoutingKey 为 “key”,第二条消息的 RoutingKey 为 “key2”,两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,由于第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,全部第二条消息被直接丢弃了。测试
那么,如何让消息被路由到队列后再返回ACK呢?或者没法被路由的消息帮我想办法处理一下?最起码通知我一声,我好本身处理啊。优化
别慌别慌,RabbitMQ里有两个机制恰好能够解决咱们上面的疑问:ui
一、mandatory 参数 二、备份交换机this
设置 mandatory 参数能够在当消息传递过程当中不可达目的地时将消息返回给生产者。
当把 mandotory 参数设置为 true 时,若是交换机没法将消息进行路由时,会将该消息返回给生产者,而若是该参数设置为false,若是发现消息没法进行路由,则直接丢弃。
那么如何设置这个参数呢?在发送消息的时候,只须要在初始化方法添加一行代码便可:
rabbitTemplate.setMandatory(true);
复制代码
开启以后咱们再从新运行前面的代码:
消息id:19729f33-15c4-4c1b-8d48-044c301e2a8e, msg:1
消息id:4aea5c57-3e71-4a7b-8a00-1595d2b568eb, msg:1
消息确认成功, id:19729f33-15c4-4c1b-8d48-044c301e2a8e
Returned message but no callback available
消息确认成功, id:4aea5c57-3e71-4a7b-8a00-1595d2b568eb
收到业务消息:1
复制代码
咱们看到中间多了一行提示 Returned message but no callback available
这是什么意思呢?
咱们上面提到,设置 mandatory 参数后,若是消息没法被路由,则会返回给生产者,是经过回调的方式进行的,因此,生产者须要设置相应的回调函数才能接受该消息。
为了进行回调,咱们须要实现一个接口 RabbitTemplate.ReturnCallback
。
@Slf4j
@Component
public class BusinessMsgProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
private void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(this);
}
public void sendCustomMsg(String exchange, String msg) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key", msg, correlationData);
correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key2", msg, correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData != null ? correlationData.getId() : "";
if (b) {
log.info("消息确认成功, id:{}", id);
} else {
log.error("消息未成功投递, id:{}, cause:{}", id, s);
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息被服务器退回。msg:{}, replyCode:{}. replyText:{}, exchange:{}, routingKey :{}",
new String(message.getBody()), replyCode, replyText, exchange, routingKey);
}
}
复制代码
而后咱们再来从新运行一次:
消息id:2e5c336a-883a-474e-b40e-b6e3499088ef, msg:1
消息id:85c771cb-c88f-47dd-adea-f0da57138423, msg:1
消息确认成功, id:2e5c336a-883a-474e-b40e-b6e3499088ef
消息没法被路由,被服务器退回。msg:1, replyCode:312. replyText:NO_ROUTE, exchange:rabbitmq.tx.demo.simple.business.exchange, routingKey :key2
消息确认成功, id:85c771cb-c88f-47dd-adea-f0da57138423
收到业务消息:1
复制代码
能够看到,咱们接收到了被退回的消息,并带上了消息被退回的缘由:NO_ROUTE
。可是要注意的是, mandatory 参数仅仅是在当消息没法被路由的时候,让生产者能够感知到这一点,只要开启了生产者确认机制,不管是否设置了 mandatory 参数,都会在交换机接收到消息时进行消息确认回调,并且一般消息的退回回调会在消息的确认回调以前。
有了 mandatory 参数,咱们得到了对没法投递消息的感知能力,有机会在生产者的消息没法被投递时发现并处理。但有时候,咱们并不知道该如何处理这些没法路由的消息,最多打个日志,而后触发报警,再来手动处理。而经过日志来处理这些没法路由的消息是很不优雅的作法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦并且容易出错。
并且设置 mandatory 参数会增长生产者的复杂性,须要添加处理这些被退回的消息的逻辑。若是既不想丢失消息,又不想增长生产者的复杂性,该怎么作呢?
前面在设置死信队列的文章中,咱们提到,能够为队列设置死信交换机来存储那些处理失败的消息,但是这些不可路由消息根本没有机会进入到队列,所以没法使用死信队列来保存消息。
不要慌,在 RabbitMQ 中,有一种备份交换机的机制存在,能够很好的应对这个问题。
什么是备份交换机呢?备份交换机能够理解为 RabbitMQ 中交换机的“备胎”,当咱们为某一个交换机声明一个对应的备份交换机时,就是为它建立一个备胎,当交换机接收到一条不可路由消息时,将会将这条消息转发到备份交换机中,由备份交换机来进行转发和处理,一般备份交换机的类型为 Fanout ,这样就能把全部消息都投递到与其绑定的队列中,而后咱们在备份交换机下绑定一个队列,这样全部那些原交换机没法被路由的消息,就会都进入这个队列了。固然,咱们还能够创建一个报警队列,用独立的消费者来进行监测和报警。
听的不太明白?不要紧,看个图就知道是怎么回事了。
(emmm,调整了一下配色,感受仍是很丑- - 。急需一个UI来拯救我。)
接下来,咱们就来设置一下备份交换机:
@Configuration
public class RabbitMQConfig {
public static final String BUSINESS_EXCHANGE_NAME = "rabbitmq.backup.test.exchange";
public static final String BUSINESS_QUEUE_NAME = "rabbitmq.backup.test.queue";
public static final String BUSINESS_BACKUP_EXCHANGE_NAME = "rabbitmq.backup.test.backup-exchange";
public static final String BUSINESS_BACKUP_QUEUE_NAME = "rabbitmq.backup.test.backup-queue";
public static final String BUSINESS_BACKUP_WARNING_QUEUE_NAME = "rabbitmq.backup.test.backup-warning-queue";
// 声明业务 Exchange
@Bean("businessExchange")
public DirectExchange businessExchange(){
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(BUSINESS_EXCHANGE_NAME)
.durable(true)
.withArgument("alternate-exchange", BUSINESS_BACKUP_EXCHANGE_NAME);
return (DirectExchange)exchangeBuilder.build();
}
// 声明备份 Exchange
@Bean("backupExchange")
public FanoutExchange backupExchange(){
ExchangeBuilder exchangeBuilder = ExchangeBuilder.fanoutExchange(BUSINESS_BACKUP_EXCHANGE_NAME)
.durable(true);
return (FanoutExchange)exchangeBuilder.build();
}
// 声明业务队列
@Bean("businessQueue")
public Queue businessQueue(){
return QueueBuilder.durable(BUSINESS_QUEUE_NAME).build();
}
// 声明业务队列绑定关系
@Bean
public Binding businessBinding(@Qualifier("businessQueue") Queue queue, @Qualifier("businessExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key");
}
// 声明备份队列
@Bean("backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BUSINESS_BACKUP_QUEUE_NAME).build();
}
// 声明报警队列
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(BUSINESS_BACKUP_WARNING_QUEUE_NAME).build();
}
// 声明备份队列绑定关系
@Bean
public Binding backupBinding(@Qualifier("backupQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
// 声明备份报警队列绑定关系
@Bean
public Binding backupWarningBinding(@Qualifier("warningQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
}
复制代码
这里咱们使用 ExchangeBuilder
来建立交换机,并为其设置备份交换机:
.withArgument("alternate-exchange", BUSINESS_BACKUP_EXCHANGE_NAME);
复制代码
为业务交换机绑定了一个队列,为备份交换机绑定了两个队列,一个用来存储不可投递消息,待以后人工处理,一个专门用来作报警用途。
接下来,分别为业务交换机和备份交换机建立消费者:
@Slf4j
@Component
public class BusinessMsgConsumer {
@RabbitListener(queues = BUSINESS_QUEUE_NAME)
public void receiveMsg(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("收到业务消息:{}", msg);
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
复制代码
@Slf4j
@Component
public class BusinessWaringConsumer {
@RabbitListener(queues = BUSINESS_BACKUP_WARNING_QUEUE_NAME)
public void receiveMsg(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.error("发现不可路由消息:{}", msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
复制代码
接下来咱们分别发送一条可路由消息和不可路由消息:
@Slf4j
@Component
public class BusinessMsgProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendCustomMsg(String exchange, String msg) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key", msg, correlationData);
correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key2", msg, correlationData);
}
}
复制代码
消息以下:
消息id:5c3a33c9-0764-4d1f-bf6a-a00d771dccb4, msg:1
消息id:42ac8c35-1d0a-4413-a1df-c26a85435354, msg:1
收到业务消息:1
发现不可路由消息:1
复制代码
这里仅仅使用 error 日志配合日志系统进行报警,若是是敏感数据,可使用邮件、钉钉、短信、电话等报警方式来提升时效性。
那么问题来了,mandatory 参数与备份交换机能够一块儿使用吗?设置 mandatory 参数会让交换机将不可路由消息退回给生产者,而备份交换机会让交换机将不可路由消息转发给它,那么若是二者同时开启,消息究竟何去何从??
emmm,想这么多干吗,试试不就知道了。
修改一下生产者便可:
@Slf4j
@Component
public class BusinessMsgProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
private void init() {
// rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(this);
}
public void sendCustomMsg(String exchange, String msg) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key", msg, correlationData);
correlationData = new CorrelationData(UUID.randomUUID().toString());
log.info("消息id:{}, msg:{}", correlationData.getId(), msg);
rabbitTemplate.convertAndSend(exchange, "key2", msg, correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
String id = correlationData != null ? correlationData.getId() : "";
if (b) {
log.info("消息确认成功, id:{}", id);
} else {
log.error("消息未成功投递, id:{}, cause:{}", id, s);
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息被服务器退回。msg:{}, replyCode:{}. replyText:{}, exchange:{}, routingKey :{}",
new String(message.getBody()), replyCode, replyText, exchange, routingKey);
}
}
复制代码
再来测试一下:
消息id:0a3eca1e-d937-418c-a7ce-bfb8ce25fdd4, msg:1
消息id:d8c9e010-e120-46da-a42e-1ba21026ff06, msg:1
消息确认成功, id:0a3eca1e-d937-418c-a7ce-bfb8ce25fdd4
消息确认成功, id:d8c9e010-e120-46da-a42e-1ba21026ff06
发现不可路由消息:1
收到业务消息:1
复制代码
能够看到,两条消息均可以收到确认成功回调,可是不可路由消息不会被回退给生产者,而是直接转发给备份交换机。可见备份交换机的处理优先级更高。
上一篇中,咱们介绍了事务机制和生产者确认机制来确保消息的可靠投递,相对而言,生产者确认机制更加高效和灵活。本篇中,咱们介绍了另外两种确保生产者的消息不丢失的机制,即经过 mandatory 参数和备份交换机来处理不可路由消息。
经过以上几种机制,咱们总算是能够确保消息被万无一失的投递到目的地了。到此,咱们的消息可靠投递也就告一段落了。消息可靠投递是咱们使用MQ时没法逃避的话题,一次性搞定它,就不会再为其所困。总的来讲,方法总比问题多,但若是你不知道这些方法,那么当问题来临时,也许就会不知所措了。
相信经过这几篇关于 RabbitMQ 文章的学习,对于 RabbitMQ 的理解已经突破天际,那还在等什么,赶忙把接入 RabbitMQ 的项目好好优化一下吧,相信如今你就不会再被那些不知所云的配置和代码所迷惑了。
到此为止,本篇就完美落幕了,但愿能给你带来一些启发,也欢迎关注个人公众号进行留言交流。