RabbitMQ(二)消息可靠投递

RabbitMQ(三)-消息可靠性投递 在中,通过工作原理图,我们了解到一个消息从产生到投递成功要经过的几个环节。同样,保证这几个环节的成功执行也是保证消息可靠投递的重要因素。 阅读详情

可靠投递

先来说明一个概念,什么是可靠投递呢?在RabbitMQ中,一个消息从生产者发送到RabbitMQ服务器,需要经历这么几个步骤:

  • 生产者准备好需要投递的消息。
  • 生产者与RabbitMQ服务器建立连接。
  • 生产者发送消息。
  • RabbitMQ服务器接收到消息,并将其路由到指定队列。
  • RabbitMQ服务器发起回调,告知生产者消息发送成功。

所谓可靠投递,就是确保消息能够百分百从生产者发送到服务器。

在这里插入图片描述
为了避免争议,补充说明一下,如果没有设置Mandatory参数,是不需要先路由消息才发起回调的,服务器收到消息后就会进行回调确认。

2、3、5步都是通过TCP连接进行交互,有网络调用的地方就会有事故,网络波动随时都有可能发生,不管是内部机房停电,还是外部光缆被切,网络事故无法预测,虽然这些都是小概率事件,但对于订单等敏感数据处理来说,这些情况下导致消息丢失都是不可接受的。

RabbitMQ中的消息可靠投递

默认情况下,发送消息的操作是不会返回任何信息给生产者的,也就是说,默认情况下生产者是不知道消息有没有正确地到达服务器。

那么如何解决这个问题呢?

对此,RabbitMQ中有一些相关的解决方案:

  • 使用事务机制来让生产者感知消息被成功投递到服务器。
  • 通过生产者确认机制实现。

在RabbitMQ中,所有确保消息可靠投递的机制都会对性能产生一定影响,如使用不当,可能会对吞吐量造成重大影响,只有通过执行性能基准测试,才能在确定性能与可靠投递之间的平衡。

在使用可靠投递前,需要先思考以下问题:

  1. 消息发布时,保证消息进入队列的重要性有多高?
  2. 如果消息无法进行路由,是否应该将该消息返回给发布者?
  3. 如果消息无法被路由,是否应该将其发送到其他地方稍后再重新进行路由?
  4. 如果RabbitMQ服务器崩溃了,是否可以接受消息丢失?
  5. RabbitMQ在处理新消息时是否应该确认它已经为发布者执行了所有请求的路由和持久化?
  6. 消息发布者是否可以批量投递消息?
  7. 在可靠投递上是否有可以接受的平衡性?是否可以接受一部分的不可靠性来提升性能?

只考虑平衡性不考虑性能是不行的,至于这个平衡的度具体如何把握,就要具体情况具体分析了,比如像订单数据这样敏感的信息,对可靠性的要求自然要比一般的业务消息对可靠性的要求高的多,因为订单数据是跟钱直接相关的,可能会导致直接的经济损失。

所以不仅应该知道有哪些保证消息可靠性的解决方案,还应该知道每种方案对性能的影响程度,以此来进行方案的选择。

RabbitMQ的事务机制

RabbitMQ是支持AMQP事务机制的,在生产者确认机制之前,事务是确保消息被成功投递的唯一方法。

在SpringBoot项目中,使用RabbitMQ事务其实很简单,只需要声明一个事务管理的Bean,并将RabbitTemplate的事务设置为true即可。

配置文件如下:

spring:
  rabbitmq:
    host: localhost
    password: guest
    username: guest
    listener:
      type: simple
      simple:
        default-requeue-rejected: false
        acknowledge-mode: manual

先来配置一下交换机和队列,以及事务管理器。

@Configuration
public class RabbitMQConfig {

    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 FanoutExchange businessExchange(){
        return new FanoutExchange(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") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }


    /**
     * 配置启用rabbitmq事务
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory) {
        return new RabbitTransactionManager(connectionFactory);
    }
}

然后创建一个消费者,来监听消息,用以判断消息是否成功发送。

@Slf4j
@Component
public class BusinessMsgConsumer {


    @RabbitListener(queues = BUSINESS_QUEUEA_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 BusinessMsgProducer{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    private void init() {
        rabbitTemplate.setChannelTransacted(true);
    }

    @Transactional
    public void sendMsg(String msg) {
        rabbitTemplate.convertAndSend(BUSINESS_EXCHANGE_NAME, "key", msg);
        log.info("msg:{}", msg);
        if (msg != null && msg.contains("exception"))
            throw new RuntimeException("surprise!");
        log.info("消息已发送 {}" ,msg);
    }
}

这里有两个注意的地方:

  1. 在初始化方法里,通过使用rabbitTemplate.setChannelTransacted(true); 来开启事务。
  2. 在发送消息的方法上加上 @Transactional 注解,这样在该方法中发生异常时,消息将不会发送。

在controller中加一个接口来生产消息:

@RestController
public class BusinessController {

    @Autowired
    private BusinessMsgProducer producer;

    @RequestMapping("send")
    public void sendMsg(String msg){
        producer.sendMsg(msg);
    }
}

来验证一下:

msg:1
消息已发送 1
收到业务消息:1
msg:2
消息已发送 2
收到业务消息:2
msg:3
消息已发送 3
收到业务消息:3
msg:exception

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: surprise!] with root cause

java.lang.RuntimeException: surprise!
	at com.mfrank.rabbitmqdemo.producer.BusinessMsgProducer.sendMsg(BusinessMsgProducer.java:30)
    ...

当 msg 的值为 exception 时, 在调用rabbitTemplate.convertAndSend 方法之后,程序抛出了异常,消息并没有发送出去,而是被当前事务回滚了。

当然,你可以将事务管理器注释掉,或者将初始化方法的开启事务注释掉,这样事务就不会生效,即使在调用了发送消息方法之后,程序发生了异常,消息也会被正常发送和消费。

RabbitMQ中的事务使用起来虽然简单,但是对性能的影响是不可忽视的,因为每次事务的提交都是阻塞式的等待服务器处理返回结果,而默认模式下,客户端是不需要等待的,直接发送就完事了,除此之外,事务消息需要比普通消息多4次与服务器的交互,这就意味着会占用更多的处理时间,所以如果对消息处理速度有较高要求时,尽量不要采用事务机制。

RabbitMQ的生产者确认机制

RabbitMQ中的生产者确认功能是AMQP规范的增强功能,当生产者发布给所有队列的已路由消息被消费者应用程序直接消费时,或者消息被放入队列并根据需要进行持久化时,一个Basic.Ack请求会被发送到生产者,如果消息无法路由,代理服务器将发送一个Basic.Nack RPC请求用于表示失败。然后由生产者决定该如何处理该消息。

也就是说,通过生产者确认机制,生产者可以在消息被服务器成功接收时得到反馈,并有机会处理未被成功接收的消息。

在Springboot中开启RabbitMQ的生产者确认模式也很简单,只多了一行配置:

spring:
  rabbitmq:
    host: localhost
    password: guest
    username: guest
    listener:
      type: simple
      simple:
        default-requeue-rejected: false
        acknowledge-mode: manual
    ## publisher-confirms: true 即表示开启生产者确认模式
    publisher-confirms: true

然后将消息生产者的代表进行部分修改:

@Slf4j
@Component
public class BusinessMsgProducer implements RabbitTemplate.ConfirmCallback{

    @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);
    }
    
    @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);
        }
    }
}

让生产者继承自RabbitTemplate.ConfirmCallback 类,然后实现其confirm 方法,即可用其接收服务器回调。

需要注意的是,在发送消息时,代码也进行了调整:

CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());

rabbitTemplate.convertAndSend(exchange, "key", msg, correlationData);

这里我们为消息设置了消息ID,以便在回调时通过该ID来判断是对哪个消息的回调,因为在回调函数中,我们是无法直接获取到消息内容的,所以需要将消息先暂存起来,根据消息的重要程度,可以考虑使用本地缓存,或者存入Redis中,或者Mysql中,然后在回调时更新其状态或者从缓存中移除,最后使用定时任务对一段时间内未发送的消息进行重新投递。

此外,生产者确认机制跟事务是不能一起工作的,是事务的轻量级替代方案。因为事务和发布者确认模式都是需要先跟服务器协商,对信道启用的一种模式,不能对同一个信道同时使用两种模式。

在生产者确认模式中,消息的确认可以是异步和批量的,所以相比使用事务,性能会更好。

使用事务机制和生产者确认机制都能确保消息被正确的发送至RabbitMQ,这里的“正确发送至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,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

听的不太明白?没关系,看个图就知道是怎么回事了。

在这里插入图片描述
接下来,我们就来设置一下备份交换机:

@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 参数和备份交换机来处理不可路由消息。

通过以上几种机制,我们总算是可以确保消息被万无一失的投递到目的地了。

生物信息学新手必看:MAFFT多序列比对从安装到实战(附参数优化技巧) 本文为生物信息学新手提供了MAFFT多序列比对工具的完整指南。从Linux环境下的安装部署,到核心算法策略(如FFT-NS-i、L-INS-i)的选择逻辑,再到空位罚分等关键参数的深度解析与优化技巧,手把手教你完成从原始序列到高质量比对结果的实战流程,为后续进化树构建等分析奠定坚实基础。 阅读详情

相关推荐

解决vscode 连接服务器报错:The remote host may not meet VS Code Server‘s prerequisites for glibc and libstdc++

解决vscode远程连接服务器报错:The remote host may not meet VS Code Server's prerequisites for glibc and libstdc++ (The remote host does not meet the prerequisites for running VS Code Server)

qq_37200100的博客 9909

RabbitMQ消息可靠性投递详解

在使用MQ实现异步通信的过程中,有消息丢了怎么办?或者MQ消息重复了怎么办? 1. 可靠性投递分析 这个问题就需要来到本文的主题——RabbitMQ的可靠性投递。当然,RabbitMQ在设计的时候就考虑了这一点,提供 了很多保证消息可靠投递的机制。这个可以说是RabbitMQ比较突出的一个特性。 下面从RabbitMQ的工作模型分析一下可靠性措施。 在我们使用RabbitMQ发送消息的时候,有几个主要环节: ①代表消息从生产者发送到Broker 生产者把消息发到Broker之后,怎么知道自己的消息.

Java是世界上最好的语言 3167

天祥TX-1C单片机开发板-秒表程序

郭天祥TX-1C开发板实现秒表程序 附开发板图片和原理图 代码 #include <reg52.h> sbit key1=P3^4; //按键S2 sbit key2=P3^5; //按键S3 sbit U1=P2^6; //LED位选 sbit U2=P2^7; //LED片选 unsigned char code ledchar[]={ /...

Run_long_road 3511

RabbitMQ可靠性投递

RabbitMQ可靠性投递

渡灬魂的博客 1483

3.ESP32S3 AP 配网开发实战:HTTP 与 WebSocket 协议实现

本文介绍了基于ESP32S3的AP配网实现方案,通过HTTP协议提供配网网页,利用WebSocket实现双向通信。详细解析了HTTP和WebSocket协议特点,阐述了SPIFFS文件系统存储HTML网页的方法,包括分区表配置、CMakeLists设置和代码实现。工程采用模块化设计,包含WiFi管理、Web服务器、IO扩展等组件,支持手机浏览器直接配网。测试验证了从AP热点连接到路由器切换的全流程,并总结了SPIFFS操作、WebSocket通信等核心优化点。该方案无需专用APP,适合物联网设备快速部署场景

u011697185的专栏 958

JWT实战:5分钟搞定Spring Boot集成JWT认证(含完整代码示例)

本文详细介绍了如何在Spring Boot中快速集成JWT认证,提供完整的代码示例和配置指南。通过JWT实现无状态认证,解决分布式系统中的会话管理问题,提升系统扩展性和安全性。涵盖令牌生成、验证、Spring Security整合及生产级优化方案。

weixin_29327525的博客 456

RabbitMQ消息的可靠投递

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。如果出现异常,则在catch中调用 basicNack,拒绝消息,让MQ重新发送消息。4.队列和交互机绑定。

Knight 1179

VSAN 虚拟机丢失排查与恢复完整方案

VSAN虚拟机(含vCenter)显示无效 检查存储状态:登录ESXi Host Client,确认VSAN存储是否挂载。 若未挂载,通过SSH执行 手动挂载。 验证文件存在:检查虚拟机文件是否完整。 解除文件锁定:删除残留的 .lck 文件 手动注册虚拟机:强制注册虚拟机配置文件。 检查VSAN健康:验证集群节点在线、磁盘组状态正常,并测试节点间网络。 修复元数据:针对损坏对象,谨慎执行修复。 终极恢复:若数据丢失,从备份还原或联系VMware支持,提交日志

sido777的博客 2679

RabbitMQ可靠性投递与实践经验

目录 1、什么是消息的可靠性投递 1.1 消息可靠性投递目的 1.2 可靠性投递流程 2、消息可靠性投递的四个确保 2.1. 确保消息发送到MQ服务器 2.2. 确保消息路由到正确的队列 2.3. 确保消息在队列持久化存储 2.4 确保消息从队列正确地投递到消费者 3、消息可靠性投递的其他手段 3.1. 消费者回调 3.2. 消费者补偿机制 3.3. 消息幂等性 3.4. 消息的顺序性 1、什么是消息的可靠性投递 1.1 消息可靠性投递目的 关于消息的可靠性投递,从以下.

长沙老码农 588

大学生数学建模赛题解析及优秀论文-2021电工杯A题高铁牵引供电系统运行数据分析及等值建模(附Python代码)

我国是世界上电气化铁路运营里程最长、服役电力机车型号最多、运营最 繁忙的国家。截至 2020年底,我国铁路年消耗电量约800亿千瓦时,约占三峡 年总发电量的 80%。图1为高速铁路牵引供电系统示意图,主要包括牵引变电所、牵引网和动车 组。牵引变电所将 220kV三相电压转换为满足电力机车运行的27.5kV单相电压, 其核心元件是牵引变压器。牵引网由馈线、接触网、钢轨、回流线等组成,完成 对电力机车的送电任务。...

wenyusuran的专栏 3169

Vue2 - 详细实现网页 “开启录音并生成音频文件,上传到服务器效果“,浏览器录制外部音频调用麦克风并传到后端,兼容全部浏览器、支持 PC 网页或手机 H5 网页、可本地播放试听(详细示例运行代码)

vue,nuxt,uniapp,网页网站,麦克风,网页录音,MediaRecorder,浏览器录音,移动端h5网页,手机端录音,web网页录音,recorder-core官方文档,Recorder,非https怎么本地录音,录音报错失败,录音后怎么播放音频,如何获取录音mp3文件,解决一直不提示麦克风权限,小白详细教程,实时录音,调用浏览器麦克风录音,浏览器不兼容,wav,mp3,vue怎么实现网页录音呢,vue2网页录音并上传到服务器,web如何实现录制音频,vue网页录音,音频录制播放和上传服务器,vu

🏆 前端领域优质创作者 6003

【RabbitMQ】消息可靠性投递(二)Exchange->Queue

第二个环节就是消息从交换机路由到队列。在什么情况下,消息会无法路由到正确的队列?可能因为路由键错误,或者队列不存在。有两种方式处理无法路由的消息: 一种就是让服务端回发给生产者 一种是让交换机路由到另一个备份的交换机 1.消息回发 消息回发的方式:使用 mandatory 参数和 ReturnListener public class ReturnListenerProducer { public static void main(String[] args) throws Exception{

A minor 608

芯片测试:WAT、CP、FT

最近部门来了一个日本回来的同事,虽然他尽量用非常Poor的中文给我解释一些东西,其中还夹杂着一些英文让我很受挫,于是最近来学一下WAT中的常用的单词含义。直接去查缩写、查单词很难去记住,要把具体的东西放在具体的场景,引发知识的连接才会在脑子里留下一定的映像。感兴趣的一起来学习一下这篇不错的文章吧。注释是我查询记录,如有不妥,欢迎指出修正。

叫好与叫座虽然不是对立面,但想在同一个作品中达到双重效果很难。 5079

RabbitMQ消息中间件(二) RabbitMQ如何保证消息的可靠性投递

RabbitMQ如何保证消息投递的准确性? 生产端的可靠性投递: 1.保证消息成功发送 2.保证MQ节点成功接收 3.发送端收到MQ节点(Broker)确认应答 4.完善的消息补偿机制 BAT等大厂解决方案: 1.消息落库,对消息状态进行打标(数据信息和消息信息落库) 2.消息的延迟投递,做二次确认,回调检查( 保证MQ在高并发的场景下) 幂等性...

xy294636185的博客 344

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

RabbitMQ学习总结(二)——Confirm消息确认与可靠性投递

RabbitMQ学习总结 前面讲到了使用消息队列解耦时,生产端只需负责将消息投递到消息队列就完事了,那么必须要考虑的问题就是如何保证生产端将消息可靠的投递到了MQ中 AMQP协议以及RabbitMQ考虑到了这一问题,因此在设计时添加了Confirm消息确认机制和Return机制,首先先讲一下这两个机制 一. Confirm消息确认机制和Return机制 Confirm消息确认机制: 生产者向MQ投递完消息后,要求MQ返回一个应答,生产者异步接收该应答,用来确定该消息是否正常的发送到了Broker, 从而保障

wanger61的博客 1501

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

rabbitmq可靠性投递_RabbitMQ消息中间件 高级篇二 高并发情况下保障消息投递可靠性...

原标题:RabbitMQ消息中间件 高级篇二 高并发情况下保障消息投递可靠性RabbitMQ消息中间件技术精讲9 高级篇二高并发场景下,消息的延迟投递做二次确认进行回调检查来保障生产者消息投递成功的可靠性在上一篇文章中,我们介绍了BAT大厂中一种方式保障生成者消息投递可靠性。思考:在上一篇中可靠性投递,在高并发的场景下是否合适?其实在上一篇文章中,我们实际上对数据库操作了两次:业务数据入库和消息信...

weixin_39814925的博客 126

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

RabbitMQ消息100%可靠性投递的解决方案实现(二)

消费端自定义监听 继承DefaultConsumer,重写handleDelivery()方法 public class Producer { public static final String MQ_HOST = "192.168.222.101"; public static final String MQ_VHOST = "/"; public static fin...

eluanshi12的博客 2312

java rabbitmq 并发_RabbitMQ消息中间件 高级篇二 高并发情况下保障消息投递可靠性...

RabbitMQ消息中间件技术精讲9 高级篇二高并发场景下,消息的延迟投递做二次确认进行回调检查来保障生产者消息投递成功的可靠性在上一篇文章中,我们介绍了BAT大厂中一种方式保障生成者消息投递可靠性。思考:在上一篇中可靠性投递,在高并发的场景下是否合适?其实在上一篇文章中,我们实际上对数据库操作了两次:业务数据入库和消息信息入库。这中对数据库多次操作的,如果在高并发场景下会出现问题的。所以我们可以...

weixin_31179927的博客 244

RabbitMQ 消息的可靠投递

目录概述方案一 消息状态打标方案二 延时再次投递消息说明   概述 可靠性投递 保障消息能够成功发出 保 障rabbitmq(broker)能够成功接收。接收指的是:broker接收到生产者发送的消息,放到exchange中,分发给对应的queue,交付给对应的消费者。 发送端要收到broker的确认应答,确认broker已收到|处理消息 完善的消息补偿机制。发送端没收到broker的确认应答,不知道消息是否成功投递成功,这时候就需要做一些补偿处理,比如重新投递。 rabbitmq的se

chy-x 的博客 1798

rabbitmq可靠性投递_RabbitMQ 可靠性投递

起因:在实际项目开发过程中,需要使用RabbitMQ来实现消息队列的功能,但仅仅实现功能之后并不能对自己满足,既然学一次,就要更深的了解她,吃一吃架构方面的相关内容,提升自己。1. 什么是生产端的可靠性投递消费端:之前在做消费端的消息确认的时候有ACK和NACK的机制,来确保消息的消费应答生产端:provider将消息发送给broker,在这个环节如何保证消息的可靠投递如果因为投递不成功,那么消息...

weixin_31865183的博客 185

深入浅出 RabbitMQ-消息可靠性投递

本文探讨了RabbitMQ消息可靠性投递机制,重点分析了消息投递路径中的关键控制点。主要内容包括: 生产者到交换机的ConfirmCallback确认机制 交换机到队列的ReturnCallback路由失败处理 队列持久化和消费者ACK机制保障消息不丢失 SpringBoot环境下的实战配置与代码示例 通过ConfirmCallback和ReturnCallback双重保障,结合消息持久化与消费者确认机制,可构建完整的消息可靠性投递方案,确保分布式系统中的消息不丢失。文章还提供了异常场景模拟和具体的补偿处理

chandfy的博客 1946

Rabbitmq消息可靠投递和重复消费等问题解决方案

消息的可靠性投递 在一些对数据一致性要求较高的业务场景里面,如果消息在发布和消费过程中出现了问题(消息丢失,消息重复消费),就会导致数据不一致,要做到消息的可靠性投递。 在RabbitMq里面提供了很多保证消息可靠投递的机制,这也是RabbitMq的一个特性。 我们在讲可靠性投递的时候,必须要明确一个问题,因为效率与可靠性是无法兼得的,如果要保证每一个环节都成功,势必会对消息的收发效率造成影响。所以如果是一些业务实时一致性要求不是特别高的场合,可以牺牲一些可靠性来换取效率。 RabbitMq的功能模型:

qq_40837310的博客 6013

RabbitMQ如何保证发送的消息可靠(重要)

RabbitMQ如何保证发送的消息可靠(RabbitMQ的Confirm模式和2.Return模式)

qq_46112274的博客 2350

消息可靠投递实践:如何保证 RabbitMQ 的消息可靠投递

死信队列”其实就是一个普通的 RabbitMQ 队列,我们可以在创建一个队列的时候为它指定 x-dead-letter-exchange 和 x-dead-letter-routing-key 参数,这样当这个队列中的消息因为各种原因被拒绝,并且设置的是不重新入队,那么 RabbitMQ 就会自动地将这个消息重新发布到我们指定的 exchange,并且用我们指定的 routing key 进行路由,从而实现消息的“死信”处理。在每次消费者准备处理消息的时候,我们都检查 "retry_count" 的值。

pengyi123的博客 1731

RabbitMQ消息100%可靠性投递的解决方案实现(一)

RabbitMQ消息中间件极速入门与实战 课程 RabbitMQ学习——高级特性 RabbitMQ消息中间件技术精讲(三)—— 深入RabbitMQ高级特性 https://blog.youkuaiyun.com/u012211603/article/details/86118129 消息如何保证100%投递成功 什么是生产端的可靠性投递 保障消息的成功发出 保障MQ节点的成功接收 发送端收到MQ节点(Br...

eluanshi12的博客 7278

RabbitMQ如何保证消息的可靠性投递

RabbitMQ如何保证消息的可靠性投递 目前来说,现在有两种方案实施: 1.数据库持久化方案 2.消息延迟投递方案 数据库持久化方案 流程: 1.将业务订单数据和生成的Message进行持久化操作(一般情况下插入数据库,这里如果分库的话可能涉及到分布式事务) 2.将Message发送到Broker服务器中 3.通过RabbitMQ的Confirm机制,在producer端,监听服务器是否A...

qq_18483301的博客 3173
上一篇: MySQL(五)一致性非锁定读与一致性锁定读
下一篇: RabbitMQ(三)消费端ack机制
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值