8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能

8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 "发布确认" 的功能

文章目录

  • [8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 "发布确认" 的功能](#8. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能)
  • [1. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 "发布确认" 的功能](#1. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 “发布确认” 的功能)
    • [1.1 回退消息](#1.1 回退消息)
  • 2.备用交换机
  • [3. API说明](#3. API说明)
  • [4. 补充:RabbitMQ 队列具体参数属性详细说明](#4. 补充:RabbitMQ 队列具体参数属性详细说明)
  • [5. 最后:](#5. 最后:)

1. RabbitMQ 消息队列 + 结合配合 Spring Boot 框架实现 "发布确认" 的功能

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:

在该文章当中:我们也是配置了"发布确认",但是当时没有结合 Spring Boot 框架实现,这里我们结合 Spring Boot 框架将其实现。

确认机制方案:

代码架构图

首先:我们需要在application.properties配置文件当中需要添加 spring.rabbitmq.publisher-confirm-type=correlated 配置信息

其中:spring.rabbitmq.publisher-confirm-type 的值有多个:

  • **NONE:**禁用发布确认模式,是默认值
  • CORRELATED: 发布消息成功到交换器后会触发回调方法
  • SIMPLE:

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

编写对应的配置类

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.config;


import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 结合 Spring Boot 实现"发布确认" 的配置类
 */

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


    // 将队列根据 routingKey 进行一个与交换机绑定
    @Bean
    public Binding queueBindingExchange(
            @Qualifier("confirmQueue") Queue confirmQueue,
            @Qualifier("confirmExchange") DirectExchange confirmExchange) {
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
    }
}

编写生产者------发送消息:

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.controller;


import com.rainbowsea.rabbitmq.springbootrabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
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;


/**
 * 结合 Spring Boot 实现"发布确认" 的生产者,发送消息
 */
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProducerController {

    @Resource
    private RabbitTemplate rabbitTemplate;


    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY, message);
        log.info("发送消息内容:{}", message);

    }
}

编写消费者------读取/接受/消费消息:

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.consumer;


import com.rainbowsea.rabbitmq.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;


/**
 * 结合 Spring Boot 实现"发布确认"  消费者------读取/消费消息
 */
@Component
@Slf4j
public class Consumer {


    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)  // 监听哪个队列
    public void receiveConfirmMessage(Message message) {
        String msg = new String(message.getBody());

        log.info("接受到的队列的 confirm.queue 队列的消息: {}", msg);

    }
}

编写,回调接口,交换机不管是否收到消息的一个回调方法我们需要 implements RabbitTemplate.ConfirmCallback 这个接口

ConfirmCallback接口

这是 RabbitTemplate 内部的一个接口,源代码如下:

java 复制代码
	/**
	 * A callback for publisher confirmations.
	 *
	 */
	@FunctionalInterface
	public interface ConfirmCallback {

		/**
		 * Confirmation callback.
		 * @param correlationData correlation data for the callback.
		 * @param ack true for ack, false for nack
		 * @param cause An optional cause, for nack, when available, otherwise null.
		 */
		void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause);

	}

生产者端发送消息之后,回调confirm()方法

  • ack参数值为true:表示消息成功发送到了交换机
  • ack参数值为false:表示消息没有发送到交换机

要点1

@Component 注解,加入IOC容器,让Spring Boot 可以读取加载该类。

要点2

配置类自身实现 ConfirmCallback、ReturnCallback 这两个接口,然后通过 this 指针把配置类的对象设置到RabbitTemplate对象中。

操作封装到了一个专门的void init()方法中。

为了保证这个void init()方法在应用启动时被调用,我们使用 @PostConstruct 注解来修饰这个方法。

关于@PostConstruct 注解大家可以参照以下说明:

@PostConstruct注解是Java中的一个标准注解,它用于指定在对象创建之后立即执行的方法。当使用依赖注入(如Spring框架)或者其他方式创建对象时,@PostConstruct注解可以确保在对象完全初始化之后,执行相应的方法。

使用@PostConstruct注解的方法必须满足以下条件:

  1. 方法不能有任何参数。
  2. 方法必须是非静态的。
  3. 方法不能返回任何值。

当容器实例化一个带有@PostConstruct注解的Bean时,它会在调用构造函数之后,并在依赖注入完成之前调用被@PostConstruct注解标记的方法。这样,我们可以在该方法中进行一些初始化操作,比如读取配置文件、建立数据库连接等。

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.callback;

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;


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


    @Resource  // 注入:
    private RabbitTemplate rabbitTemplate;


    @PostConstruct  // 表示一开始就会执行该函数当中的方法 进行一个初始化
    public void init() {
        // 注入: 因为我们的这个 ConfirmCallback 是  RabbitTemplate 的内部接口,
        // 我们无法直接对 RabbitTemplate 的内部接口进行一个直接的 注入。只能
        // 通过先对 RabbitTemplate 进行一个 注入,在将 ConfirmCallback set 进入到已经注入的 RabbitTemplate 类当中
        rabbitTemplate.setConfirmCallback(this);
    }


    /**
     * 交换机确认回调方法
     * 1.发消息,交换机收到了,回调该方法
     * 第一个参数 CorrelationData 保存回调消息的ID,以及相关信息
     * 第二个参数: 交换机是否成功接受到了消息,true 表示接受到了,false 表示没有接受到
     * 第三个参数: 表示没有接受到消息的原因,成功了就为 null
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已经收到 ID 为: {} 的消息", id);
        } else {
            log.info("交换机没有收到 ID 为: {} 的消息,由于原因:{}", id, cause);
        }
    }
}

修改一下,生产者的代码,将 ID 发送给交换机。

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.controller;


import com.rainbowsea.rabbitmq.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.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;


/**
 * 结合 Spring Boot 实现"发布确认" 的生产者,发送消息
 */
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProducerController {

    @Resource
    private RabbitTemplate rabbitTemplate;


    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY, message,correlationData);
        log.info("发送消息内容:{}", message);

    }
}

1.1 回退消息

Mandatory 参数

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被交换机给丢弃了 。那么如何让无法被路由的消息帮我们处理呢,最起码通知生产者一声,好让生产者知道,或者可以再发一次。可以通过设置 mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者

我们需要在 application.properties 配置文件当中,加上 spring.rabbitmq.publisher-returns=true

properties 复制代码
spring.rabbitmq.host=192.168.76.156
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true

当然,也可以在:实现 implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback 的类当中,加入这个 rabbitTemplate.setMandatory(true);

ReturnCallback接口

同样也 RabbitTemplate 内部的一个接口,源代码如下:

java 复制代码
	/**
	 * A callback for returned messages.
	 *
	 * @since 2.3
	 */
	@FunctionalInterface
	public interface ReturnsCallback {

		/**
		 * Returned message callback.
		 * @param returned the returned message and metadata.
		 */
		void returnedMessage(ReturnedMessage returned);

	}

注意:接口中的returnedMessage()方法仅在消息没有发送到队列时调用

ReturnedMessage类中主要属性含义如下:

属性名 类型 含义
message org.springframework.amqp.core.Message 消息以及消息相关数据
replyCode int 应答码,类似于HTTP响应状态码
replyText String 应答码说明
exchange String 交换机名称
routingKey String 路由键名称

和 ConfiremCallback接口时一样的

java 复制代码
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;


@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {


    @Resource  // 注入:
    private RabbitTemplate rabbitTemplate;




    @PostConstruct  // 表示一开始就会执行该函数当中的方法 进行一个初始化
    public void init() {
        // 注入: 因为我们的这个 ConfirmCallback 是  RabbitTemplate 的内部接口,
        // 我们无法直接对 RabbitTemplate 的内部接口进行一个直接的 注入。只能
        // 通过先对 RabbitTemplate 进行一个 注入,在将 ConfirmCallback set 进入到已经注入的 RabbitTemplate 类当中
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 可以在当消息传递过程中不可达目的地时,将消息返回给生产者
     * 只有不可达目的的时候,才进行回退,执行该函数
     * @param returned
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.error("消息{},被交换机 {} 回退,回退的原因:{},路由Key:{}",new String(returned.getMessage().getBody()),
                returned.getExchange(),returned.getReplyText(),returned.getRoutingKey()
                );
    }
}

完整配置代码内容:

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.callback;

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;


@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {


    @Resource  // 注入:
    private RabbitTemplate rabbitTemplate;




    @PostConstruct  // 表示一开始就会执行该函数当中的方法 进行一个初始化
    public void init() {
        // 注入: 因为我们的这个 ConfirmCallback 是  RabbitTemplate 的内部接口,
        // 我们无法直接对 RabbitTemplate 的内部接口进行一个直接的 注入。只能
        // 通过先对 RabbitTemplate 进行一个 注入,在将 ConfirmCallback set 进入到已经注入的 RabbitTemplate 类当中
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 可以在当消息传递过程中不可达目的地时,将消息返回给生产者
     * 只有不可达目的的时候,才进行回退,执行该函数
     * @param returned
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        log.error("消息{},被交换机 {} 回退,回退的原因:{},路由Key:{}",new String(returned.getMessage().getBody()),
                returned.getExchange(),returned.getReplyText(),returned.getRoutingKey()
                );
    }


    /**
     * 交换机确认回调方法
     * 1.发消息,交换机收到了,回调该方法
     * 第一个参数 CorrelationData 保存回调消息的ID,以及相关信息
     * 第二个参数: 交换机是否成功接受到了消息,true 表示接受到了,false 表示没有接受到
     * 第三个参数: 表示没有接受到消息的原因,成功了就为 null
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已经收到 ID 为: {} 的消息", id);
        } else {
            log.info("交换机没有收到 ID 为: {} 的消息,由于原因:{}", id, cause);
        }
    }


}

修改生产者代码,向一个不存在的 routingKey 发送消息,模拟,交换机接收不到该消息,执行回调。

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.controller;


import com.rainbowsea.rabbitmq.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.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;


/**
 * 结合 Spring Boot 实现"发布确认" 的生产者,发送消息
 */
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProducerController {

    @Resource
    private RabbitTemplate rabbitTemplate;


    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY, message,correlationData);
        log.info("发送消息内容:{}", message);


        CorrelationData correlationData2 = new CorrelationData("2");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY + 2, message + "key 2",correlationData2);
        log.info("发送消息内容:{}", message);
    }
}

浏览器访问:http://localhost:8080/confirm/sendMessage/Hello World

在这里我们为什么要创建这个配置类呢?首先,我们需要声明回调函数来接收RabbitMQ服务器返回的确认信息:

方法名 方法功能 所属接口 接口所属类
confirm() 确认消息是否发送到交换机 ConfirmCallback RabbitTemplate
returnedMessage() 确认消息是否发送到队列 ReturnsCallback RabbitTemplate

然后,就是对 RabbitTemplate的功能进行增强,因为回调函数所在对象必须设置到 RabbitTemplate对象中才能生效。

原本RabbitTemplate对象并没有生产者端消息确认的功能,要给它设置对应的组件才可以。

而设置对应的组件,需要调用RabbitTemplate对象下面两个方法:

设置组件调用的方法 所需对象类型
setConfirmCallback() ConfirmCallback接口类型
setReturnCallback() ReturnCallback接口类型

2.备用交换机

有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息 无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者 所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的 复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。 在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份 交换机可以理解为 RabbitMQ 中交换机的"备胎",当我们为某一个交换机声明一个对应的备份交换机时, 就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由 备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑 定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都 进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。

代码架构图:

编写备用交换机的,配置类

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.config;


import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfirmConfig2 {

    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    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 queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }

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

    //声明确认 Exchange 交换机同时配置的备份交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        ExchangeBuilder exchangeBuilder =
                ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                        .durable(true)
                        // 设置该交换机的备份交换机
                        .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return (DirectExchange) exchangeBuilder.build();
    }

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

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

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

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

}

默认交换机是:fanout 扇出的,不要特别配置。

编写报警消费者,读取(warningQueue)报警队列当中的消息

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.consumer;


import com.rainbowsea.rabbitmq.springbootrabbitmq.config.ConfirmConfig2;
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 WarningConsumer {


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

    }
}

生产者,还是沿用上面回退消息的 ,没有修改

java 复制代码
package com.rainbowsea.rabbitmq.springbootrabbitmq.controller;


import com.rainbowsea.rabbitmq.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.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;


/**
 * 结合 Spring Boot 实现"发布确认" 的生产者,发送消息
 */
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProducerController {

    @Resource
    private RabbitTemplate rabbitTemplate;


    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        CorrelationData correlationData = new CorrelationData("1");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY, message, correlationData);
        log.info("发送消息内容:{}", message);


        CorrelationData correlationData2 = new CorrelationData("2");
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,
                ConfirmConfig.CONFIRM_ROUTING_KEY + "2", message + "key 2", correlationData2);
        log.info("发送消息内容:{}", message);
    }
}

重新启动项目的时候需要把原来的 confirm.exchange 删除因为我们修改了其绑定属性,不然报以下错:

运行测试:

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

3. API说明

3.1 ConfirmCallback接口

这是 RabbitTemplate 内部的一个接口,源代码如下:

java 复制代码
	/**
	 * A callback for publisher confirmations.
	 *
	 */
	@FunctionalInterface
	public interface ConfirmCallback {

		/**
		 * Confirmation callback.
		 * @param correlationData correlation data for the callback.
		 * @param ack true for ack, false for nack
		 * @param cause An optional cause, for nack, when available, otherwise null.
		 */
		void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause);

	}

生产者端发送消息之后,回调confirm()方法

  • ack参数值为true:表示消息成功发送到了交换机
  • ack参数值为false:表示消息没有发送到交换机

3.2 ReturnCallback接口

同样也 RabbitTemplate 内部的一个接口,源代码如下:

java 复制代码
	/**
	 * A callback for returned messages.
	 *
	 * @since 2.3
	 */
	@FunctionalInterface
	public interface ReturnsCallback {

		/**
		 * Returned message callback.
		 * @param returned the returned message and metadata.
		 */
		void returnedMessage(ReturnedMessage returned);

	}

注意:接口中的returnedMessage()方法仅在消息没有发送到队列时调用

ReturnedMessage类中主要属性含义如下:

属性名 类型 含义
message org.springframework.amqp.core.Message 消息以及消息相关数据
replyCode int 应答码,类似于HTTP响应状态码
replyText String 应答码说明
exchange String 交换机名称
routingKey String 路由键名称

3.3 配置类属性说明

要点1

@Component 注解,加入IOC容器,让Spring Boot 可以读取加载该类。

要点2

配置类自身实现 ConfirmCallback、ReturnCallback 这两个接口,然后通过 this 指针把配置类的对象设置到RabbitTemplate对象中。

操作封装到了一个专门的void init()方法中。

为了保证这个void init()方法在应用启动时被调用,我们使用 @PostConstruct 注解来修饰这个方法。

关于@PostConstruct 注解大家可以参照以下说明:

@PostConstruct注解是Java中的一个标准注解,它用于指定在对象创建之后立即执行的方法。当使用依赖注入(如Spring框架)或者其他方式创建对象时,@PostConstruct注解可以确保在对象完全初始化之后,执行相应的方法。

使用@PostConstruct注解的方法必须满足以下条件:

  1. 方法不能有任何参数。
  2. 方法必须是非静态的。
  3. 方法不能返回任何值。

当容器实例化一个带有@PostConstruct注解的Bean时,它会在调用构造函数之后,并在依赖注入完成之前调用被@PostConstruct注解标记的方法。这样,我们可以在该方法中进行一些初始化操作,比如读取配置文件、建立数据库连接等。

bindings属性

  • 表面作用:
    • 指定交换机和队列之间的绑定关系
    • 指定当前方法要监听的队列
  • 隐藏效果:如果RabbitMQ服务器上没有这里指定的交换机和队列,那么框架底层的代码会创建它们

queues属性

java 复制代码
@RabbitListener(queues = {QUEUE_ATGUIGU})
  • 作用:指定当前方法要监听的队列
  • 注意:此时框架不会创建相关交换机和队列,必须提前创建好

3.4 相关API

先回到PPT理解"deliveryTag:交付标签机制"

下面我们探讨的三个方法都是来自于com.rabbitmq.client.Channel接口

basicAck()方法

  • 方法功能:给Broker返回ACK确认信息,表示消息已经在消费端成功消费,这样Broker就可以把消息删除了
  • 参数列表:
参数名称 含义
long deliveryTag Broker给每一条进入队列的消息都设定一个唯一标识
boolean multiple 取值为true:为小于、等于deliveryTag的消息批量返回ACK信息 取值为false:仅为指定的deliveryTag返回ACK信息

basicNack()方法

  • 方法功能:给Broker返回NACK信息,表示消息在消费端消费失败,此时Broker的后续操作取决于参数requeue的值
  • 参数列表:
参数名称 含义
long deliveryTag Broker给每一条进入队列的消息都设定一个唯一标识
boolean multiple 取值为true:为小于、等于deliveryTag的消息批量返回ACK信息 取值为false:仅为指定的deliveryTag返回ACK信息
boolean requeue 取值为true:Broker将消息重新放回队列,接下来会重新投递给消费端 取值为false:Broker将消息标记为已消费,不会放回队列

basicReject()方法

  • 方法功能:根据指定的deliveryTag,对该消息表示拒绝
  • 参数列表:
参数名称 含义
long deliveryTag Broker给每一条进入队列的消息都设定一个唯一标识
boolean requeue 取值为true:Broker将消息重新放回队列,接下来会重新投递给消费端 取值为false:Broker将消息标记为已消费,不会放回队列
  • basicNack()和basicReject()有啥区别?
    • basicNack()有批量操作
    • basicReject()没有批量操作

4. 补充:RabbitMQ 队列具体参数属性详细说明

  • Type:队列类型
  • Name:队列名称,就是一个字符串,随便一个字符串就可以;
  • Durability:声明队列是否持久化,代表队列在服务器重启后是否还存在;
  • Auto delete: 是否自动删除,如果为true,当没有消费者连接到这个队列的时候,队列会自动删除;
  • Exclusive:exclusive属性的队列只对首次声明它的连接可见,并且在连接断开时自动删除;
  • 基本上不设置它,设置成false
  • Arguments:队列的其他属性,例如指定DLX(死信交换机等);
  1. x-expires:Number 当Queue(队列)在指定的时间未被访问,则队列将被自动删除;

  2. x-message-ttl:Number 发布的消息在队列中存在多长时间后被取消(单位毫秒);

  3. x-overflow:String 设置队列溢出行为,当达到队列的最大长度时,消息会发生什么,有效值为Drop Head或Reject Publish;

  4. x-max-length:Number 队列所能容下消息的最大长度,当超出长度后,新消息将会覆盖最前面的消息,类似于Redis的LRU算法;

  5. x-single-active-consumer 默认为false激活单一的消费者,也就是该队列只能有一个消息者消费消息;

  6. **x-max-length-bytes:Number ** 限定队列的最大占用空间,当超出后也使用类似于Redis的LRU算法;

  7. x-dead-letter-exchange:String 指定队列关联的死信交换机,有时候我们希望当队列的消息达到上限后溢出的消息不会被删除掉,而是走到另一个队列中保存起来;

  8. x-dead-letter-routing-key:String 指定死信交换机的路由键,一般和6一起定义;

  9. x-max-priority:Number 如果将一个队列加上优先级参数,那么该队列为优先级队列;

    1. 给队列加上优先级参数使其成为优先级队列 x-max-priority=10【0-255取值范围】
    2. 给消息加上优先级属性:通过优先级特性,将一个队列实现插队消费;
java 复制代码
MessageProperties messageProperties=new MessageProperties(); messageProperties.setPriority(8);
  1. x-queue-mode:String (理解下即可)队列类型x-queue-mode=lazy懒队列,在磁盘上尽可能多地保留消息以减少RAM使用,如果未设置,则队列将保留内存缓存以尽可能快地传递消息;

  2. x-queue-master-locator:String (用的较少,不讲)

在集群模式下设置队列分配到的主节点位置信息;

每个queue都有一个master节点,所有对于queue的操作都是事先在master上完成,之后再slave上进行相同的操作;

每个不同的queue可以坐落在不同的集群节点上,这些queue如果配置了镜像队列,那么会有1个master和多个slave。

基本上所有的操作都落在master上,那么如果这些queues的master都落在个别的服务节点上,而其他的节点又很空闲,这样就无法做到负载均衡,那么势必会影响性能;

关于 master queue host 的分配有几种策略,可以在queue声明的时候使用 x-queue-master-locator 参数,或者在 policy 上设置 queue-master-locator ,或者直接在 rabbitmq 的配置文件中定义 queue_master_locator ,有三种可供选择的策略:

  1. min-masters:选择master queue数最少的那个服务节点host;
  2. client-local:选择与client相连接的那个服务节点host;
  3. random:随机分配;

5. 最后:

"在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。"

相关推荐
276695829215 分钟前
海关 瑞数 后缀分析 rs
java·python·rs·瑞数·海关·瑞数后缀·后缀生成
刘某的Cloud16 分钟前
rabbitmq常用命令
linux·运维·分布式·rabbitmq·系统
呼Lu噜20 分钟前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
bing_15825 分钟前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
小臭希27 分钟前
Java——琐碎知识点一
java·开发语言
星星点点洲37 分钟前
【RabbitMQ】保证消息不丢失
rabbitmq
学c真好玩38 分钟前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia041238 分钟前
GenericObjectPool——重用你的对象
后端
Piper蛋窝1 小时前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel1 小时前
招幕技术人员
前端·javascript·后端