(十一)发布确认深入

发布确认深入

  • 前言
  • 1、发布确认
    • 1.1、实现机制
    • 1.2、配置文件
    • 1.3、代码实现
    • 1.4、回退消息
      • 1. Mandatory 参数
      • 2、代码实现
    • 1.5、备份交换机
      • 1、修改配置类
      • 2、报警消费者

前言

当RabbitMQ宕机后, 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。这是比较麻烦,所以该怎样才能使 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?

1、发布确认

1.1、实现机制

在之前都没有考虑MQ宕机的情况,如果消息发送了,MQ宕机了,那么消息就会丢失,这里为了解决这个问题,所以消息在发送的时候都进行一个备份,然后发送给MQ,只有MQ收到消息确认之后才能将缓存中备份的数据进行删除,有效的防止消息丢失;
(十一)发布确认深入_第1张图片

发布确认是发消息到MQ的保障,手动应答是防止消费者嘎了,MQ到消费者消息不丢失的保障

1.2、配置文件

代码架构图

在这里插入图片描述

配置文件修改

在配置文件当中需要添加

spring.rabbitmq.publisher-confirm-type=correlated
  • NONE
    禁用发布确认模式,是默认值

  • CORRELATED
    发布消息成功到交换器后会触发回调方法

  • SIMPLE
    经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
    其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker

1.3、代码实现

配置类

package com.feng.springbootrabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author Feng
 * @Date 2022/11/26 15:27
 * @Version 1.0
 * @Description 发布确认(高级) 配置u类
 */
@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String CONFIRM_ROUTING_KEY = "key1";

    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    @Bean
    public Binding queueBindingExchange(@Qualifier("confirmQueue")Queue queue, @Qualifier("confirmExchange") DirectExchange directExchange){
        return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);
    }

}

交换机回调接口

package com.feng.springbootrabbitmq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Author Feng
 * @Date 2022/11/26 16:09
 * @Version 1.0
 * @Description 交换机收到发送者消息得回调类
 */
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {

    //因为这个回调接口是内部接口,所以实现类得注入这个接口
    @Resource
    private RabbitTemplate rabbitTemplate;
    //construct>@Autowired> @PostConstruct
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1.发消息,交换机收到消息 回调
     *
     * @param correlationData 消息的ID以及消息内容
     * @param ack             交换机是否收到消息
     * @param cause           收到消息失败得原因,成功为NULL
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        //对 correlationData 进行判空
        String id = correlationData != null ? correlationData.getId() : "";
        //成功收到消息
        if (ack) {
            log.info("交换机已经收到 id 为:{}的消息", id);
        }else {
            //没有收到消息
            log.info("交换机还未收到 id 为:{}消息,原因:{}", id, cause);
        }
    }
}

生产者

package com.feng.springbootrabbitmq.controller;

import com.feng.springbootrabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 消息回调和退回
     *
     * @param message
     */
    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        //这个类就是消息的ID以及小心哦内容
        CorrelationData correlationData =new CorrelationData("1");
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, ConfirmConfig.CONFIRM_ROUTING_KEY, message,correlationData);
        log.info(ConfirmConfig.CONFIRM_ROUTING_KEY + "发送消息内容:{}", message);
    }
}

消费者

package com.feng.springbootrabbitmq.consumer;

import com.feng.springbootrabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class ConfirmConsumer {

    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message) {
        String msg = new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}", msg);
    }

}

问题

假设发送了两条消息,第一条消息的 RoutingKey 为 “key1”,第二条消息的 RoutingKey 为 “key2”,两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。

丢弃的消息交换机是不知道的,需要解决告诉生产者消息传送失败

原因
因为这个回调接口是在交换机收到生产者发送的消息后进行回调的,但是如果队列出现了问题,这个时候消息就会出现丢失,生产者是不知道的,那么就只能靠回退消息进行处理

1.4、回退消息

1. Mandatory 参数

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者

2、代码实现

配置文件

spring.rabbitmq.publisher-returns=true

回退接口

低版本可能没有 RabbitTemplate.ReturnsCallback 请用RabbitTemplate.ReturnCallback

package com.feng.springbootrabbitmq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Author Feng
 * @Date 2022/11/26 16:09
 * @Version 1.0
 * @Description 交换机收到发送者消息得回调类
 */
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {

    //因为这个回调接口是内部接口,所以实现类得注入这个接口
    @Resource
    private RabbitTemplate rabbitTemplate;
    //construct>@Autowired> @PostConstruct
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        /**
         * true:交换机无法将消息进行路由时,会将该消息返回给生产者
         * false:如果发现消息无法进行路由,则直接丢弃
         */
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(this);
        //RabbitMQ版本低的是 rabbitTemplate.setReturnCallback(myCallBack);
    }

    /**
     * 交换机确认回调方法
     * 1.发消息,交换机收到消息 回调
     *
     * @param correlationData 消息的ID以及消息内容
     * @param ack             交换机是否收到消息
     * @param cause           收到消息失败得原因,成功为NULL
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        //对 correlationData 进行判空
        String id = correlationData != null ? correlationData.getId() : "";
        //成功收到消息
        if (ack) {
            log.info("交换机已经收到 id 为:{}的消息", id);
        }else {
            //没有收到消息
            log.info("交换机还未收到 id 为:{}消息,原因:{}", id, cause);
        }
    }

    /**
     * 当消息无法路由的时候的回调方法
     * 只有消息不可达目的地的时候才触发
     * @param returned
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.error("消息:{},被交换机 {} 退回,原因:{},路由key:{},code:{}",
                new String(returned.getMessage().getBody()),
                returned.getExchange(),
                returned.getReplyText(),
                returned.getRoutingKey(),
                returned.getReplyCode());
    }
}

再次测试,发送一个正确路由,一个错误理由的两个消息

(十一)发布确认深入_第2张图片

结果

(十一)发布确认深入_第3张图片

1.5、备份交换机

当消息路由不可路由的时候我们只能回退给生产者进行处理,而生产者最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。也会增加生产者的代码逻辑。

在 RabbitMQ 中,有一种备份交换机的机制存在。当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

代码架构图

(十一)发布确认深入_第4张图片

1、修改配置类

这里修改了原有交换机的配置,所以记得将原有的交换机删掉

package com.feng.springbootrabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author Feng
 * @Date 2022/11/26 15:27
 * @Version 1.0
 * @Description 发布确认(高级) 配置u类
 */
@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    public static final String CONFIRM_ROUTING_KEY = "key1";

    //备份交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    //备份队列
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    //警告队列
    public static final String WARNING_QUEUE_NAME = "warning.queue";



    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    @Bean
    public Binding queueBindingExchange(@Qualifier("confirmQueue")Queue queue, @Qualifier("confirmExchange") DirectExchange directExchange){
        return BindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);
    }

    //声明确认交换机,以及和备份交换机的联系
    @Bean("confirmExchange")
    public DirectExchange backupConfirmExchange() {
        ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                .durable(true)
                //设置该交换机的备份交换机
                //或者直接用方法 .alternate("备份交换机名")
                .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return exchangeBuilder.build();
    }

    //************************以下是关于备份的******************************

    //声明备份 Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    // 声明备份队列
    @Bean("backQueue")
    public Queue backQueue() {
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    // 声明警告队列
    @Bean("warningQueue")
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    // 声明备份队列绑定关系
    @Bean
    public Binding backupBinding(@Qualifier("backQueue") Queue queue,
                                 @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

    // 声明报警队列绑定关系
    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
                                  @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

}

2、报警消费者

package com.feng.springbootrabbitmq.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @Author Feng
 * @Date 2022/11/26 17:35
 * @Version 1.0
 * @Description 报警队列消费者
 */
@Component
@Slf4j
public class WarningConsumer {

    public static final String WARNING_QUEUE_NAME = "warning.queue";

    @RabbitListener(queues = WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message) {
        String msg = new String(message.getBody());
        log.error("报警发现不可路由消息:{}", msg);
    }
}

测试

http://localhost:8080/confirm/sendMessage/喜喜

(十一)发布确认深入_第5张图片

思考
mandatory 参数(消息回退给生产者)与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。

你可能感兴趣的:(RabbitMQ,java-rabbitmq,rabbitmq,java)