【RabbitMQ】如何进行消息可靠投递【下篇】
说明
上一篇文章里,我们了解了如何保证消息被可靠投递到RabbitMQ的交换机中,但还有一些不完美的地方,试想一下,如果向RabbitMQ服务器发送一条消息,服务器确实也接收到了这条消息,于是给你返回了ACK确认消息,但服务器拿到这条消息一看,找不到路由它的队列,于是就把它丢进了垃圾桶,emmm,我猜应该属于可回收垃圾。
如何让消息可靠投递到队列
如果你对上面的描述还不是很清楚,那我再用代码来说明一次。
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时,生产者是不知道消息被丢弃这个事件的。
我们将上一篇中的交换机类型改为DirectExchange,这样就只有当消息的 RoutingKey 和队列绑定时设置的 Bindingkey (这里即“key”)一致时,才会真正将该消息进行路由。
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里有两个机制刚好可以解决我们上面的疑问:
1、mandatory 参数 2、备份交换机
mandatory 参数
设置 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 的项目好好优化一下吧,相信现在你就不会再被那些不知所云的配置和代码所迷惑了。
到此为止,本篇就完美落幕了,希望能给你带来一些启发,也欢迎关注我的公众号进行留言交流。
【RabbitMQ】如何进行消息可靠投递【下篇】的更多相关文章
- 【RabbitMQ】如何进行消息可靠投递【上篇】
说明 前几天,突然发生线上报警,钉钉连发了好几条消息,一看是RabbitMQ相关的消息,心头一紧,难道翻车了? [橙色报警] 应用[xxx]在[08-15 16:36:04]发生[错误日志异常],al ...
- springboot + rabbitmq发送邮件(保证消息100%投递成功并被消费)
前言: RabbitMQ相关知识请参考: https://www.jianshu.com/p/cc3d2017e7b3 Linux安装RabbitMQ请参考: https://www.jianshu. ...
- (转载)springboot + rabbitmq发送邮件(保证消息100%投递成功并被消费)
转载自https://www.jianshu.com/p/dca01aad6bc8 一.先扔一张图 image.png 说明: 本文涵盖了关于RabbitMQ很多方面的知识点, 如: 消息发送确认 ...
- RabbitMQ如何保证发送端消息的可靠投递
消息发布者向RabbitMQ进行消息投递时默认情况下是不返回发布者该条消息在broker中的状态的,也就是说发布者不知道这条消息是否真的抵达RabbitMQ的broker之上,也因此会发生消息丢失的情 ...
- IM系统中如何保证消息的可靠投递(即QoS机制)(转)
消息的可靠性,即消息的不丢失和不重复,是im系统中的一个难点.当初qq在技术上(当时叫oicq)因为以下两点原因才打败了icq:1)qq的消息投递可靠(消息不丢失,不重复)2)qq的垃圾消息少(它an ...
- IM系统中如何保证消息的可靠投递(即QoS机制)
消息的可靠性,即消息的不丢失和不重复,是im系统中的一个难点.当初qq在技术上(当时叫oicq)因为以下两点原因才打败了icq:1)qq的消息投递可靠(消息不丢失,不重复)2)qq的垃圾消息少(它 ...
- 2.RabbitMQ 的可靠性消息的发送
本篇包含 1. RabbitMQ 的可靠性消息的发送 2. RabbitMQ 集群的原理与高可用架构的搭建 3. RabbitMQ 的实践经验 上篇包含 1.MQ 的本质,MQ 的作用 2.R ...
- RabbitMQ如何保证发送端消息的可靠投递-发生镜像队列发生故障转移时
上一篇最后提到了mandatory这个参数,对于设置mandatory参数个人感觉还是很重要的,尤其在RabbitMQ镜像队列发生故障转移时. 模拟个测试环境如下: 首先在集群队列中增加两个镜像队列的 ...
- RabbitMQ 消息的可靠投递
mq 提供了两种方式确认消息的可靠投递 confirmCallback 确认模式 returnCallback 未投递到 queue 退回模式 在使用 RabbitMQ 的时候,作为消息发送方希望杜绝 ...
随机推荐
- springboot-权限控制shiro(一)
1. 场景描述 (1)权限控制是IT项目特别是企业项目,绕不开的重要模块,接下来结合springboot介绍下权限控制框架shiro. (2)springboot集成shiro的东西有点多,一篇博客完 ...
- 深挖Openstack Nova - Scheduler调度策略
深挖Openstack Nova - Scheduler调度策略 一. Scheduler的作用就是在创建实例(instance)时,为实例选择出合适的主机(host).这个过程分两步:过滤(F ...
- 关于JLINK调试时出现的 erasing range....的问题结果方法
声明:本人当然不是提倡盗版. 昨天在使用JLINK的时候遇到了这个问题,但是非常蹊跷,首先可以下载,但不能进入调试,到后来完成不能下载了. 这个问题的原因就是你得Keil检测到你锁使用的JLINK不是 ...
- TestNG中DataProvider的用法二:简单的数据驱动
@DataProvider标记的方法除了可以返回数组外,还可以返回一个Iterator,这样的好处是不用把所有的测试数据都加载到内存中,而是需要的时候就读一条. 下面的例子就使用了Iterator,然 ...
- 基于高德开放平台的 NODE 天气信息组件
看看了画在手上的Armani手表,马上就快到了下班的时间了,心里总觉的空唠唠的, 好像空缺了什么一样,仔细的想了一想,微微叹了一口气,觉得是时候在这里和大家分享一下原因了........ 首先: ## ...
- Linux平台 Oracle 19c RAC安装Part1:准备工作
一.实施前期准备工作 1.1 服务器安装操作系统 1.2 Oracle安装介质 1.3 共享存储规划 1.4 网络规范分配 二.安装前期准备工作 2.1 各节点系统时间校对 2.2 各节点关闭防火墙和 ...
- log4net服务启动后没有记录日志
有时候在用log4net的时候,调试或执行是ok的,但是安装服务后没有记录日志. 这是因为服务启动是在C盘启动,而程序放的位置在别的目录. 这时候需要指定读取配置文件的位置为程序所在的目录 strin ...
- what is the CCA?
Clear Channel Assessment (CCA) is one of two carrier sense mechanisms in WLAN (or WiFi). It is defin ...
- Mysql 局域网连接设置——Windows
在公司工作中,会遇到mysql数据库存储于某个人的电脑上,大家要想连接mysql服务,装有mysql服务的电脑就必须开启远程连接. 其实不仅仅是局域网,只要你有数据库所在服务器的公网IP地址都能连上. ...
- SIMBOSS:物联网业务如何应用领域驱动设计?
前言 在这个万物互联的时代,物联网业务蓬勃发展,但也瞬息万变,对于开发人员来说,这是一种挑战,但也是一种“折磨”. 在业务发展初期,因为时间有限,我们一般会遵循“小步快跑,迭代试错”的原则进行业务开发 ...