RabbitMQ学习文档(进阶篇(Demo使用Spring编写))

一、依赖

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.14.2</version>
</dependency>

二、代码

1、死信队列

1.1、TTL过期

1.1.1、生产者
public class Provider {
    // 队列名称
    private static final String COMMON_EXCHANGE_NAME = "common_exchange";
    private static final String COMMON_QUEUE_NAME = "common_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        /**
         * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel,
         * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行
         */
        try (
                // 根据上面设置的参数来创建连接
                Connection connection = factory.newConnection();
                // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
                Channel channel = connection.createChannel();
        ) {
            /**
             * 创建交换机
             * 参数1:交换机名称
             * 参数2:交换机类型;direct代表直接模式,这种模式根据消息的路由键进行消息转发,其中消息携带路由键,而队列和交换机也通过路由键进行绑定,当两个路由键完全一致的时候,交换机就会把消息发送给这个队列
             */
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            /**
             * 消息属性
             * new AMQP.BasicProperties(XXX)是直接从MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化配置中拿过来的
             * expiration(XXX)用来设置消息过期时间,单位是毫秒
             */
            AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain",
                    null,
                    null,
                    2,
                    0, null, null, null,
                    null, null, null, null,
                    null, null).builder().expiration("10000").build();
            // 消息内容
            String message = "Hello World!";
            /**
             * 发送消息到队列
             * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列
             * 参数1:交换机名称
             *          ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机
             * 参数2:路由名称
             *          由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称
             * 参数3:其他消息属性,比如MessageProperties.PERSISTENT_TEXT_PLAIN就是消息持久化
             * 参数4,消息内容
             *          消息内容是字节数组,所以我们需要使用getBytes()方法
             */
            channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.1.2、消费者
public class Consumer {
    // 队列名称
    private static final String COMMON_EXCHANGE_NAME = "common_exchange";
    private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    private static final String COMMON_QUEUE_NAME = "common_queue";
    private static final String DEAD_QUEUE_NAME = "dead_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 创建普通交换机
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 创建死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 为死信指明死信交换机和死信路由
            Map<String, Object> arguments = new HashMap<>();
            // 设置死信的目的交换机
            arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
            // 设置死信携带的路由
            arguments.put("x-dead-letter-routing-key", DEAD_QUEUE_NAME);
            /**
             * 创建普通队列
             * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments);
            /**
             * 创建死信队列
             */
            channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null);
            // 绑定普通队列和普通交换机
            channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME);
            // 绑定死信队列和死信交换机
            channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println(str);
            };
            /**
             * 消费者异步监听队列消息
             * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息
             * 参数1:队列名称;
             *          指定消费者连接的队列
             * 参数2:是否自动确认;
             *        如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息
             *        如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息
             * 参数3:消息传递时的回调对象
             * 参数4:消费者被取消时的回调,暂时没有看懂啥意思
             */
            channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.1.3、说明

TTL代表time to live,即消息存活时间,默认情况下队列中的消息不会过期,不过部分场景需要设置过期时间,比如淘宝购买商品的时候,必须要在两个小时内付款,否则订单自动取消;又或者说在12306上买票的时候,必须在30分钟内付款,否则订单自动取消;这些过程都可以用到死信队列,毕竟这些场景的时间都是定值,所以开发者可以设置一个普通队列,该队列的消息过期时间是2小时或者30分钟,这个根据场景来定,当然这个过程时间也可以由发送者设置,比如我们上面的例子中就是由发送者设置的,但是我们现在说的场景是固定时间由普通队列设置,并且该队列和死信交换机进行绑定,并且设置了相关路由键,也就是一旦消息过期,消息将携带设置的路由键进入死信交换机,然后死信交换机把消息交给死信队列,但是这个普通队列不允许任何消费者监听,毕竟该队列就是进行计时的,不需要被监听,我们真正应该监听的是死信队列,毕竟消息过期的时候也代表时间够了,然后我们消息经由普通队列》死信交换机》死信队列》消费者,然后我们根据订单的状态来做对应的事情,比如订单已经被支付了,那我们什么都不用做,如果订单依然没有被支付,我们就可以进行取消订单操作,如果订单已经被取消了,我们也可以什么都不用做,至于这整个流程我们可以这样来看,如下:

在这里插入图片描述

1.2、消息数量超过队列最大长度

1.2.1、生产者
public class Provider {
    // 队列名称
    private static final String COMMON_EXCHANGE_NAME = "common_exchange";
    private static final String COMMON_QUEUE_NAME = "common_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        /**
         * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel,
         * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行
         */
        try (
                // 根据上面设置的参数来创建连接
                Connection connection = factory.newConnection();
                // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
                Channel channel = connection.createChannel();
        ) {
            /**
             * 创建交换机
             * 参数1:交换机名称
             * 参数2:交换机类型;direct代表直接模式,这种模式根据消息的路由键进行消息转发,其中消息携带路由键,而队列和交换机也通过路由键进行绑定,当两个路由键完全一致的时候,交换机就会把消息发送给这个队列
             */
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            for (int i = 1; i <= 10; i++) {
                // 消息内容
                String message = "Hello World!" + i;
                /**
                 * 发送消息到队列
                 * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列
                 * 参数1:交换机名称
                 *          ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机
                 * 参数2:路由名称
                 *          由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称
                 * 参数3:其他消息属性
                 *          MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化,即消息将会存储到磁盘中,只要消息队列也是持久化的,即使RabbitMQ重启,消息依然存在于队列中
                 * 参数4,消息内容
                 *          消息内容是字节数组,所以我们需要使用getBytes()方法
                 */
                channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.2.2、普通消费者
/*

 * Copyright (c) 2006-2021, BeiJing Leadal Technology Co.,Ltd.

 * All rights reserved.

 *

 * Project   : kms-wiki

 * Version   : 1.1.0

 * JDK Version : 1.6

 *

 * Comments

 *

 * History

 * Sr Date         ModifiedBy     Why & What is modified

 * 1. 2022/4/22  guoming      Created

 */
package com.atguigu.demo.dead_letter.max_queue_length;

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * 消费者(作用:用来接收普通消息;注意:该消费者只用创建交换机和队列,创建完成之后就可以关闭了)
 *
 * @author guoming
 * @date 2022/4/22 23:14
 */
public class Consumer1 {
    // 队列名称
    private static final String COMMON_EXCHANGE_NAME = "common_exchange";
    private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    private static final String COMMON_QUEUE_NAME = "common_queue";
    private static final String DEAD_QUEUE_NAME = "dead_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 创建普通交换机
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 创建死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 为死信指明死信交换机和死信路由
            Map<String, Object> arguments = new HashMap<>();
            // 设置普通队列最大长度,也就是队列同时最多能存在的消息数量
            arguments.put("x-max-length", 6);
            // 设置死信的目的交换机
            arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
            // 设置死信携带的路由
            arguments.put("x-dead-letter-routing-key", DEAD_QUEUE_NAME);
            /**
             * 创建普通队列
             * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments);
            /**
             * 创建死信队列
             */
            channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null);
            // 绑定普通队列和普通交换机
            channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME);
            // 绑定死信队列和死信交换机
            channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1.2.3、死信消费者
public class Consumer2 {
    // 队列名称
    private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    private static final String DEAD_QUEUE_NAME = "dead_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 创建死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 创建死信队列
            channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null);
            // 绑定死信队列和死信交换机
            channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println(str);
            };
            /**
             * 消费者异步监听队列消息
             * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息
             * 参数1:队列名称;
             *          指定消费者连接的队列
             * 参数2:是否自动确认;
             *        如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息
             *        如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息
             * 参数3:消息传递时的回调对象
             * 参数4:消费者被取消时的回调,暂时没有看懂啥意思
             */
            channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.2.4、说明

如果队列中的最大消息数量有限制,超出数量的消息将成为死信,我们本次的模拟办法是设置队列的最大消息数量是6,然后我们发送10条消息,那么消息队列只能存储6个,由于队列是先进先出的,所以最前面到来的4个消息将会成为死信。普通队列对应有死信交换机,并且设置了死信消息的路由是死信队列名称,这样我们就可以把死信消息发送到死信队列,然后被消费者消费掉,整个过程如下图:

在这里插入图片描述

1.3、消费者拒绝接收消息,并拒绝将消息重新放回队列

1.3.1、生产者
public class Provider {
    // 队列名称
    private static final String COMMON_EXCHANGE_NAME = "common_exchange";
    private static final String COMMON_QUEUE_NAME = "common_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        /**
         * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel,
         * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行
         */
        try (
                // 根据上面设置的参数来创建连接
                Connection connection = factory.newConnection();
                // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
                Channel channel = connection.createChannel();
        ) {
            /**
             * 创建交换机
             * 参数1:交换机名称
             * 参数2:交换机类型;direct代表直接模式,这种模式根据消息的路由键进行消息转发,其中消息携带路由键,而队列和交换机也通过路由键进行绑定,当两个路由键完全一致的时候,交换机就会把消息发送给这个队列
             */
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            for (int i = 1; i <= 10; i++) {
                // 消息内容
                String message = "Hello World!" + i;
                /**
                 * 发送消息到队列
                 * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列
                 * 参数1:交换机名称
                 *          ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机
                 * 参数2:路由名称
                 *          由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称
                 * 参数3:其他消息属性
                 *          MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化,即消息将会存储到磁盘中,只要消息队列也是持久化的,即使RabbitMQ重启,消息依然存在于队列中
                 * 参数4,消息内容
                 *          消息内容是字节数组,所以我们需要使用getBytes()方法
                 */
                channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.3.2、普通消费者
public class Consumer1 {
    // 队列名称
    private static final String COMMON_EXCHANGE_NAME = "common_exchange";
    private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    private static final String COMMON_QUEUE_NAME = "common_queue";
    private static final String DEAD_QUEUE_NAME = "dead_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 创建普通交换机
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 创建死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 为死信指明死信交换机和死信路由
            Map<String, Object> arguments = new HashMap<>();
            // 设置死信的目的交换机
            arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
            // 设置死信携带的路由
            arguments.put("x-dead-letter-routing-key", DEAD_QUEUE_NAME);
            /**
             * 创建普通队列
             * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments);
            /**
             * 创建死信队列
             */
            channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null);
            // 绑定普通队列和普通交换机
            channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME);
            // 绑定死信队列和死信交换机
            channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                /**
                 * 手动拒绝接收消息
                 * 参数1:消息标签
                 * 参数2:是否重新将消息放回原来的队列
                 *       是:消息重新放回队列,然后重新发送消费者,该消息不会生成死信消息
                 *       否:消息不放回队列,该消息会成为死信消息
                 */
                channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
            };
            /**
             * 消费者异步监听队列消息
             * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息
             * 参数1:队列名称;
             *          指定消费者连接的队列
             * 参数2:是否自动确认;
             *        如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息
             *        如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息
             * 参数3:消息传递时的回调对象
             * 参数4:消费者被取消时的回调,暂时没有看懂啥意思
             */
            channel.basicConsume(COMMON_QUEUE_NAME, false, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.3.3、死信消费者
public class Consumer2 {
    // 队列名称
    private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
    private static final String DEAD_QUEUE_NAME = "dead_queue";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 创建死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 创建死信队列
            channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null);
            // 绑定死信队列和死信交换机
            channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println(str);
            };
            /**
             * 消费者异步监听队列消息
             * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息
             * 参数1:队列名称;
             *          指定消费者连接的队列
             * 参数2:是否自动确认;
             *        如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息
             *        如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息
             * 参数3:消息传递时的回调对象
             * 参数4:消费者被取消时的回调,暂时没有看懂啥意思
             */
            channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1.3.4、说明

如果普通消费者通过basicReject()方法拒绝了消息,并且通过设置requeue参数为false,那代表不将消息放回队列,这样的话消息将成为死信,在上面的例子中,普通队列中的死信将传递给死信交换机,并且消息路由键是死信队列名称,这样的话,死信队列的消费者就可以接收到被拒绝的消息,画图说明如下:

在这里插入图片描述

2、优先级队列

2.1、生产者

public class Provider {

    // 普通交换机
    public static final String COMMON_EXCHANGE_NAME = "common_exchange3";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue3";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            // 声明交换机
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);

            // 消息发送
            for (int i = 1; i <= 10; i++) {
                String message = "测试消息" + i;
                if (i == 5) {
                    // 默认优先级都是0,设置5号消息的优先级是5
                    AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain",
                            null,
                            null,
                            2,
                            5, null, null, null,
                            null, null, null, null,
                            null, null).builder().build();
                    channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));
                } else {
                    channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.2、消费者

public class Consumer {

    // 普通交换机
    public static final String COMMON_EXCHANGE_NAME = "common_exchange3";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue3";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 声明交换机
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);
            // 创建队列
            Map<String, Object> arguments = new HashMap<>(1);
            arguments.put("x-max-priority", 10);
            channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments);
            // 绑定普通队列和普通交换机
            channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println(str);
            };
            channel.basicConsume(COMMON_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.3、说明

默认的消息优先级是0,其中消息优先级越大,在队列中越靠前,然后越早被发送,我们可以设置队列中的最大消息优先级,代码如下:

Map<String, Object> arguments = new HashMap<>(1);
arguments.put("x-max-priority", 10);
channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments);

设置之后代表发送者发送的消息最大优先级不应该超过10,其实在rabbitmq官网上也说明了消息优先级应该是1~255之间的正整数,并且队列中设置的最大优先级不应该超过10,避免优先级过多的时候将会导致 Erlang 进程消耗更多的CPU 资源,然后我们可以在发送者发送消息的时候设置消息优先级,如下:

// 默认优先级都是0,设置5号消息的优先级是5
AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain",
        null,
        null,
        2,
        5, null, null, null,
        null, null, null, null,
        null, null).builder().build();
channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));

3、自定义延迟交换机

3.1、生产者

public class Provider {

    // 延时交换机
    public static final String DELAY_EXCHANGE_NAME = "delay_exchange2";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue2";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            // 创建延迟交换机;注意交换机类型是"x-delayed-message",另外该交换机本质上还是一个direct类型交换机
            Map<String, Object> arguments = new HashMap<>(1);
            arguments.put("x-delayed-type", "direct");
            channel.exchangeDeclare(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);

            // 10s消息;说明:10s消息应该比2s的消息更晚到达
            String message = "我是10s的消息";
            HashMap<String, Object> header = new HashMap<>();
            // 设置延迟10s:这个延迟操作是在交换机中进行了,10s之后会将消息发送给消息队列,当然本次使用的交换机是延迟交换机
            header.put("x-delay", 10000);
            AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain",
                    null,
                    null,
                    2,
                    0, null, null, null,
                    null, null, null, null,
                    null, null).builder().headers(header).build();
            channel.basicPublish(DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));

            // 2s消息;说明:2s消息应该比10s的消息更早到达
            message = "我是2s的消息";
            header = new HashMap<>();
            header.put("x-delay", 2000);
            properties = new AMQP.BasicProperties("text/plain",
                    null,
                    null,
                    2,
                    0, null, null, null,
                    null, null, null, null,
                    null, null).builder().headers(header).build();
            channel.basicPublish(DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.2、消费者

public class Consumer {

    // 延时交换机
    public static final String DELAY_EXCHANGE_NAME = "delay_exchange2";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue2";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            // 创建延迟交换机
            Map<String, Object> arguments = new HashMap<>(1);
            arguments.put("x-delayed-type", "direct");
            channel.exchangeDeclare(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);
            // 创建队列
            channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, null);
            // 绑定普通队列和延迟交换机
            channel.queueBind(COMMON_QUEUE_NAME, DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println(str);
            };
            channel.basicConsume(COMMON_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.3、Windows中安装并启用延迟交换机插件

(1)首先从https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases下载对应版本的插件
在这里插入图片描述
(2)解压到rabbitmq安装目录下面的plugins目录下面
在这里插入图片描述
(3)在rabbitmq安装目录下面的sbin目录下打开DOS窗口,然后执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange就可以让插件起作用
(4)我们打开rabbitmq控制台之后,选择交换机,然后就可以看到如下的交换机类型
在这里插入图片描述

3.3、说明

在此之前,我们如果想实现延时效果,需要借助于过期时间和死信队列来打成,假设我们设置一个消息过期时间是2s,另外一个消息过期时间是10s,然后先将过期时间10s的消息发送到一个队列中,在将过期时间是2s的消息发送到同样队列中,然后让消息过期,之后通过死信交换机把消息交给死信队列,另外我们有消息着在监听着死信队列,我们肯定以为2s的消息会首先被消费,然后10s的消息在被消费,其实不是这样的,而是10s的消息被消费之后,2s的消息才被消费,原因就是队列是先进先出的,所以我们无法实现想要的效果,上面的文字用画图表示如下:

在这里插入图片描述
为了解决该问题,我们安装了延迟交换机插件,其中延时过程是在交换机中进行的,当延时结束之后就会将消息发送给消息队列,所以就不会产生上面的延时失败问题了,在代码编写过程中,可以设置交换机类型为x-delayed-message,即延时交换机,并且在消息头中设置延迟时间,如下所示:

// 创建延迟交换机;注意交换机类型是"x-delayed-message",另外该交换机本质上还是一个direct类型交换机
Map<String, Object> arguments = new HashMap<>(1);
arguments.put("x-delayed-type", "direct");
channel.exchangeDeclare(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments);

// 10s消息;说明:10s消息应该比2s的消息更晚到达
String message = "我是10s的消息";
HashMap<String, Object> header = new HashMap<>();
// 设置延迟10s:这个延迟操作是在交换机中进行了,10s之后会将消息发送给消息队列,当然本次使用的交换机是延迟交换机
header.put("x-delay", 10000);
AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain",
        null,
        null,
        2,
        0, null, null, null,
        null, null, null, null,
        null, null).builder().headers(header).build();
channel.basicPublish(DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));

4、消息无法抵达交换机或者无法抵达队列的情况,也是异步发布确认的写法

4.1、生产者

import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 生产者
 *
 * @author 明快de玄米61
 * @date 2022/4/22 23:14
 */
public class Provider {

    // 存在的交换机
    public static final String EXIST_EXCHANGE_NAME = "exist_exchange_name";

    // 不存在的队列
    public static final String NOT_EXIST_QUEUE_NAME = "not_exist_queue_name";

    public static void main(String[] args) throws InterruptedException {

        // 存储消息内容,方便异步监听时取出
        ConcurrentHashMap<Long, String> map = new ConcurrentHashMap<>();

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            // 开启确认发布,然后才会回调addConfirmListener方法
            channel.confirmSelect();

            //消息发送到交换机成功或者失败,都会调用该方法;注意:如果交换机不存在,程序将会报错,然后停止执行
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("成功,deliveryTag:" + deliveryTag + ",值:" + map.get(deliveryTag));
                    map.remove(deliveryTag);
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("失败:" + map.get(deliveryTag));
                    map.remove(deliveryTag);
                }
            });

            // 无法发送消息到队列,就会调用该方法
            channel.addReturnListener(new ReturnListener() {
                @Override
                public void handleReturn(int replyCode,
                                         String replyText,
                                         String exchange,
                                         String routingKey,
                                         AMQP.BasicProperties properties,
                                         byte[] body) throws IOException {
                    System.out.println("没找到符合的队列而退回的消息 = " + new String(body, "utf-8"));
                }
            });

            // 声明存在的交换机
            channel.exchangeDeclare(EXIST_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);

            // 当交换机发送消息到队列失败的时候,将会回调该方法,成功不会回调
            String message = "不存在的队列";
            for (int i = 1; i < 1000; i++) {
                System.out.println("标号:" + i);
                message += i;
                long nextPublishSeqNo = channel.getNextPublishSeqNo();
                // 第三个参数设置为true,将会把找不到队列的消息返还给生产者,然后上面addReturnListener方法才能起到作用
                channel.basicPublish(EXIST_EXCHANGE_NAME, NOT_EXIST_QUEUE_NAME, true, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
                map.put(nextPublishSeqNo, message);
                message = "不存在的队列";
            }

            // 同步等待,等异步监听跑完,毕竟try里面声明channel,代码执行完毕后将会关闭channel,这样会导致异步监听无法再执行
            channel.waitForConfirms();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2、说明

1、只有开启确认发布,才会回调addConfirmListener方法

在这里插入图片描述

2、只有发送消息时第三个参数设置为true,才会把找不到队列的消息返还给生产者,然后addReturnListener方法才能起到作用

在这里插入图片描述

3、在开启异步监听的时候,记得在最后开启同步等待,避免异步监听程序还没执行完,然后channel通道就被关闭了

在这里插入图片描述

4、用并发Map保证在异步监听代码执行时也能取到数据

在这里插入图片描述

5、备用交换机

5.1、生产者

public class Provider {

    // 普通交换机
    public static final String COMMON_EXCHANGE_NAME = "common_exchange5";

    // 备用交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup_exchange5";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue5";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            // 声明普通交换机
            Map<String, Object> arguments = new HashMap<>();
            arguments.put("alternate-exchange", BACKUP_EXCHANGE_NAME);
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, arguments);

            // 消息发送
            String message = "测试消息1";
            channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));

            // 消息发送(说明:我们故意将路由键写错,让交换机无法传递消息到普通队列,然后消息被被传递到了备用交换机中,然后我们的备用队列就可以收到消息了)
            message = "测试消息2";
            channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME + "1", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.2、普通消费者

public class Consumer1 {

    // 普通交换机
    public static final String COMMON_EXCHANGE_NAME = "common_exchange5";

    // 备用交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup_exchange5";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue5";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();

            // 声明普通交换机
            Map<String, Object> arguments = new HashMap<>();
            arguments.put("alternate-exchange", BACKUP_EXCHANGE_NAME);
            channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, arguments);

            // 声明备用交换机
            channel.exchangeDeclare(BACKUP_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);

            // 创建队列
            channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, null);
            // 绑定死信队列和死信交换机
            channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new java.lang.String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println("普通队列接收到的消息:" + str);
            };
            channel.basicConsume(COMMON_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.3、备用消费者

public class Consumer2 {

    // 普通交换机
    public static final String COMMON_EXCHANGE_NAME = "common_exchange5";

    // 备用交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup_exchange5";

    // 普通队列
    public static final String COMMON_QUEUE_NAME = "common_queue5";

    // 备用队列
    public static final String BACKUP_QUEUE_NAME = "backup_queue5";

    public static void main(String[] args) {
        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();

            // 声明备用交换机
            channel.exchangeDeclare(BACKUP_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);

            // 创建队列
            channel.queueDeclare(BACKUP_QUEUE_NAME, true, false, false, null);
            // 绑定死信队列和死信交换机
            channel.queueBind(BACKUP_QUEUE_NAME, BACKUP_EXCHANGE_NAME, "");
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println("备用队列接收到的消息:" + str);
            };
            channel.basicConsume(BACKUP_QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.4、说明

如果交换机无法把消息发送给队列,比如通过消息路由键无法匹配到对应队列,那么在我们设置了普通交换机对应的备用交换机的前提下,消息将会发送给备用交换机,进而发送给备用队列,然后消费者就可以消费消息,过程画图如下:

在这里插入图片描述

6、文件分片传输

6.1、依赖

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.14.2</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.16</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.7</version>
</dependency>

6.2、消息对象

import java.io.Serializable;

public class Message implements Serializable {

    // 分片文件
    private byte[] body;

    // 文件名
    private String filename;

    // 文件md5码
    private String identifier;

    // 文件uuid
    private String uuid;

    // 默认切片大小
    private long chunkSize;

    // 文件总大小
    private long totalSize;

    // 文件切片总数
    private int totalChunks;

    // 当前是第几片(切片)
    private int chunkNumber;

    // 当前切片大小
    private long currentChunkSize;

    public byte[] getBody() {
        return body;
    }

    public void setBody(byte[] body) {
        this.body = body;
    }

    public String getFilename() {
        return filename;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public String getIdentifier() {
        return identifier;
    }

    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }

    public long getChunkSize() {
        return chunkSize;
    }

    public void setChunkSize(long chunkSize) {
        this.chunkSize = chunkSize;
    }

    public long getTotalSize() {
        return totalSize;
    }

    public void setTotalSize(long totalSize) {
        this.totalSize = totalSize;
    }

    public int getTotalChunks() {
        return totalChunks;
    }

    public void setTotalChunks(int totalChunks) {
        this.totalChunks = totalChunks;
    }

    public int getChunkNumber() {
        return chunkNumber;
    }

    public void setChunkNumber(int chunkNumber) {
        this.chunkNumber = chunkNumber;
    }

    public long getCurrentChunkSize() {
        return currentChunkSize;
    }

    public void setCurrentChunkSize(long currentChunkSize) {
        this.currentChunkSize = currentChunkSize;
    }

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }
}

6.3、生产者端临时文件对象

// 已接收文件信息
public class FileInfo {
    FileInfo() {
    }

    // 已接收分片数量
    private int number;

    // 文件路径
    private String filePath;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
}

6.4、生产者

import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.crypto.digest.MD5;
import com.rabbitmq.client.*;

import java.io.File;
import java.util.Arrays;

/**
 * 生产者
 *
 * @author 明快de玄米61
 * @date 2022/4/22 23:14
 */
public class Provider {
    // 队列名称
    private static final String QUEUE_NAME = "sharding_queue_spring";

    public static void main(String[] args) {
        // 准备分片参数信息
        // 待传输文件
        File file = new File("C:\\Users\\mingming\\Downloads\\1云平台一站式部署.mp4");
        // 文件UUID,组装文件分片的唯一标识
        String uuid = UUID.fastUUID().toString();
        // 文件名
        String filename = file.getName();
        // 文件md5码
        String identifier = MD5.create().digestHex(file);
        // 默认切片大小:1M
        int chunkSize = 1024 * 1024;
        // 文件总大小
        int totalSize = (int) file.length();
        // 文件切片总数
        int totalChunks = totalSize % chunkSize == 0 ? totalSize / chunkSize : (totalSize / chunkSize + 1);
        // 文件字节数组
        byte[] fileContent = FileUtil.readBytes(file);

        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        /**
         * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel,
         * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行
         */
        try (
                // 根据上面设置的参数来创建连接
                Connection connection = factory.newConnection();
                // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
                Channel channel = connection.createChannel();
        ) {
            /**
             * 创建队列
             * 说明:一保证消息发送之前,队列已经存在了
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他之后,仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:自动删除
             *          没有消费者连接是否自动删除
             * 参数5:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            for (int i = 1; i <= totalChunks; i++) {
                byte[] body;
                if (i == totalChunks) {
                    // 最后一块
                    body = Arrays.copyOfRange(fileContent, chunkSize * (i - 1), totalSize);
                } else {
                    body = Arrays.copyOfRange(fileContent, chunkSize * (i - 1), chunkSize * i);
                }

                // 当前是第几片(切片)
                int chunkNumber = i;

                // 当前切片大小
                long currentChunkSize = body.length;

                // 组装发送数据
                Message message = new Message();
                message.setBody(body);
                message.setFilename(filename);
                message.setIdentifier(identifier);
                message.setUuid(uuid);
                message.setChunkNumber(chunkNumber);
                message.setChunkSize(chunkSize);
                message.setCurrentChunkSize(currentChunkSize);
                message.setTotalChunks(totalChunks);
                message.setTotalSize(totalSize);

                /**
                 * 发送消息到队列
                 * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列
                 * 参数1:交换机名称
                 *          ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机
                 * 参数2:路由名称
                 *          由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称
                 * 参数3:其他消息属性
                 *          MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化,即消息将会存储到磁盘中,只要消息队列也是持久化的,即使RabbitMQ重启,消息依然存在于队列中
                 * 参数4,消息内容
                 *          消息内容是字节数组,所以我们需要使用getBytes()方法
                 */
                channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, Convert.toPrimitiveByteArray(message));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("发送完成");
    }
}

6.5、消费者

import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 消费者
 *
 * @author 明快de玄米61
 * @date 2022/4/22 23:14
 */
public class Consumer {

    // 队列名称
    private static final String QUEUE_NAME = "sharding_queue_spring";

    public static void main(String[] args) {
        // 文件信息存储Map
        Map<String, FileInfo> map = new HashMap<>();

        // 创建连接工厂,配置连接相关设置
        ConnectionFactory factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost("localhost");
        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            // 根据上面设置的参数来创建连接
            Connection connection = factory.newConnection();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            Channel channel = connection.createChannel();
            /**
             * 创建队列
             * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:自动删除
             *          没有消费者连接是否自动删除
             * 参数5:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                Message msg = Convert.convert(Message.class, message.getBody());
                String uuid = msg.getUuid();

                // 处理首块分片
                if (!map.containsKey(uuid)) {
                    String tmpdirPath = System.getProperty("java.io.tmpdir");
                    String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
                    String[] params = {tmpdirPath.substring(0, tmpdirPath.length() - 1), "tmpFile", dateStr, uuid};
                    String fileUrl = String.join(File.separator, Arrays.asList(params));
                    if (!new File(fileUrl).exists()) {
                        new File(fileUrl).mkdirs();
                    }
                    String filePath = fileUrl + File.separator + msg.getFilename();
                    new File(filePath).createNewFile();
                    FileInfo fileInfo = new FileInfo();
                    fileInfo.setNumber(0);
                    fileInfo.setFilePath(filePath);
                    map.put(uuid, fileInfo);
                }

                // 接收分片
                FileInfo fileInfo = map.get(uuid);
                //第一步 打开将要写入的文件
                RandomAccessFile raf = new RandomAccessFile(fileInfo.getFilePath(), "rw");
                //第二步 打开通道
                try {
                    FileChannel fileChannel = raf.getChannel();
                    //第三步 计算偏移量
                    long position = (msg.getChunkNumber() - 1) * msg.getChunkSize();
                    //第四步 获取分片数据
                    byte[] fileData = msg.getBody();
                    //第五步 写入数据
                    fileChannel.position(position);
                    fileChannel.write(ByteBuffer.wrap(fileData));
                    fileChannel.force(true);
                    fileChannel.close();
                    fileInfo.setNumber(fileInfo.getNumber() + 1);
                } finally {
                    IoUtil.close(raf);
                }

                // 判断分片是否接收完成
                if (msg.getTotalChunks() == map.get(uuid).getNumber()) {
                    // 文件接收完成
                    System.out.println("接收文件全路径:" + fileInfo.getFilePath());
                    // 移除map中文件信息
                    map.remove(uuid);
                }
            };
            /**
             * 消费者异步监听队列消息
             * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息
             * 参数1:队列名称;
             *          指定消费者连接的队列
             * 参数2:是否自动确认;
             *        如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息
             *        如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息
             * 参数3:消息传递时的回调对象
             * 参数4:消费者被取消时的回调,暂时没有看懂啥意思
             */
            channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

7、完整版Demo

7.1、说明

完整版Demo中包含:

  • 创建连接工厂
  • 声明队列
  • 声明交换机
  • 声明队列和交换机的绑定关系
  • 发送消息
  • 消息出现问题退回
  • 能者多劳
  • 消息手动确定

7.2、依赖

<!--实现amqb协议的rabbitmq客户端依赖-->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.14.2</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.7</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.10</version>
</dependency>

7.3、配置文件

把配置文件rabbitmq.properties放在resources目录下面

mq.host=127.0.0.1
mq.port=5672
mq.username=netiler
mq.password=netilermanager

位置截图如下:

在这里插入图片描述

7.4、配置类

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

/**
 * RabbitMq配置类
 */
public class MqConfig {

	// RabbitMq或者kafka
	static String mqMode;

	// ip
	static String mqHost;

	// 端口
	static int mqPort;

	// 用户名
	static String mqUsername;

	// 密码
	static String mqPassword;

	// 通讯协议
	static boolean mqSsl;

	// vhost
	static String mqVhost;

	//MQ初始化连接数
	static int mqInitConnections;

	static {
		init();
	}

	static void init() {
		// 取出rabbitmq.properties
		Resource r = new ClassPathResource("rabbitmq.properties");
		Properties props = new Properties();
		try {
			props.load(new FileInputStream(r.getFile()));
		} catch (IOException e) {
			e.printStackTrace();
		}

		// 获取配置文件内容
		String mode = props.getProperty("mq.mode");
		String host = props.getProperty("mq.host");
		String port = props.getProperty("mq.port");
		String username = props.getProperty("mq.username");
		String password = props.getProperty("mq.password");
		String ssl = props.getProperty("mq.ssl");
		String vhost = props.getProperty("mq.vhost");
		String connections = props.getProperty("mq.connections");

		// 处理数据
		mqMode = mode != null ? mode : "rabbitmq";
		mqHost = host != null ? host : "127.0.0.1";
		mqPort = port != null ? Integer.valueOf(port) : 5672;
		mqUsername = username != null ? username : "guest";
		mqPassword = password != null ? password : "guest";
		mqSsl = Boolean.parseBoolean(ssl);
		mqVhost = vhost != null ? vhost : "/";
		mqInitConnections = connections != null ? Integer.valueOf(connections) : 10;
	}

	public static String getMqMode() {
		return mqMode;
	}

	public static void setMqMode(String mqMode) {
		MqConfig.mqMode = mqMode;
	}

	public static String getMqHost() {
		return mqHost;
	}

	public static void setMqHost(String mqHost) {
		MqConfig.mqHost = mqHost;
	}

	public static int getMqPort() {
		return mqPort;
	}

	public static void setMqPort(int mqPort) {
		MqConfig.mqPort = mqPort;
	}

	public static String getMqUsername() {
		return mqUsername;
	}

	public static void setMqUsername(String mqUsername) {
		MqConfig.mqUsername = mqUsername;
	}

	public static String getMqPassword() {
		return mqPassword;
	}

	public static void setMqPassword(String mqPassword) {
		MqConfig.mqPassword = mqPassword;
	}

	public static boolean isMqSsl() {
		return mqSsl;
	}

	public static void setMqSsl(boolean mqSsl) {
		MqConfig.mqSsl = mqSsl;
	}

	public static String getMqVhost() {
		return mqVhost;
	}

	public static void setMqVhost(String mqVhost) {
		MqConfig.mqVhost = mqVhost;
	}

	public static int getMqInitConnections() {
		return mqInitConnections;
	}

	public static void setMqInitConnections(int mqInitConnections) {
		MqConfig.mqInitConnections = mqInitConnections;
	}
}

7.5、工具类

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;

/**
 * RabbitMQ工具类
 *
 * @author 明快de玄米61
 * @date 2023/8/26 23:00
 */
public class RabbitMqUtil {

    // 连接工厂
    static ConnectionFactory factory;

    // Connection连接对象池
    private static ConcurrentLinkedQueue<Connection> connections = new ConcurrentLinkedQueue<Connection>();

    private static Map<String,Integer> connectionChannelCount = new ConcurrentHashMap<String,Integer>();

    // 线程池
    private static ExecutorService executor = Executors.newFixedThreadPool(2);

    //最小连接数
    private static int MIN_CONNECTIONS = 10;

    // 初始化连接工厂和连接对象
    static  {
        // 创建连接工厂
        factory = new ConnectionFactory();
        /**
         * 设置连接参数
         * 比如:host(localhost)、port(5672)、virtualHost(/)、
         * userName(guest)、password(guest)、connectionTimeout(60s)
         * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值,
         * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了,
         * 下面的代码作用是说明一下如何设置我们想要的值
         */
        factory.setHost(MqConfig.getMqHost()); // 默认值:localhost
        factory.setPort(MqConfig.getMqPort()); // 默认值:5672
        factory.setUsername(MqConfig.getMqUsername()); // 默认值:guest
        factory.setPassword(MqConfig.getMqPassword()); // 默认值:guest
        factory.setVirtualHost(MqConfig.getMqVhost()); // 默认值:/
        if (MqConfig.isMqSsl()) {
            try {
                factory.useSslProtocol();
            } catch (KeyManagementException | NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }

        // 设置最小连接数
        MIN_CONNECTIONS = MqConfig.getMqInitConnections();

        // 创建Connection连接对象,并放入Connection连接对象池
        init();
    }

    // 初始化操作
    private static void init() {
        for (int i = 0; i < MIN_CONNECTIONS; i++) {
            try {
                System.out.println("create default conn"+i);
                forceCreateConn();
                Thread.sleep(100l);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 强制创建Connection对象
    private static void forceCreateConn() throws IOException, TimeoutException {
        Connection conn = factory.newConnection(executor);
        conn.setId(UUID.randomUUID().toString());
        connections.add(conn);
    }

    // 创建Connection对象
    public static synchronized Connection createConn() throws IOException, TimeoutException {
        Connection conn = null;
        //初始判断队列中是否存在10个连接
        if(connections.size() < MIN_CONNECTIONS){
            //不存在,添加连接
            conn = factory.newConnection(executor);
            conn.setId(UUID.randomUUID().toString());
            connections.add(conn);
        }else{
            //存在10个,获取一个连接
            conn = connections.poll();
            if(conn == null || !conn.isOpen() ){
                //连接为空活关闭,创建连接
                if(conn != null){
                    conn.close();
                }
                conn = factory.newConnection(executor);
                conn.setId(UUID.randomUUID().toString());
                connections.add(conn);
            }else{
                //重新放到队列最后面
                connections.offer(conn);
            }
        }
        return conn;
    }

    // 创建Channel对象
    public static Channel createChannel(Connection conn) throws IOException {
        Channel channel = conn.createChannel();
        if(conn.getId()==null) {
            conn.setId(UUID.randomUUID().toString());
        }
        Integer count = connectionChannelCount.get(conn.getId());
        if(count==null) {
            count=0;
        }
        count++;
        connectionChannelCount.put(conn.getId(), count);
        return channel;
    }

    // 关闭Channel对象
    public static void close(Channel channel) {
        try {
            if (channel != null && channel.isOpen()) {
                channel.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

7.4、生产者

import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 生产者
 *
 * @author 明快de玄米61
 * @date 2022/4/22 23:14
 */
public class Provider {

    // 交换机名称
    private static final String EXCHANGE_NAME = "full_version_exchange_spring";

    // 队列名称
    private static final String QUEUE_NAME = "full_version_queue_spring";

    // 绑定键名称,指明队列和交换机的绑定关系
    private static final String BINDING_KEY = "my_key";

    // 路由键名称,对于直连交换机来说,只有 “绑定键” 全等于 “路由键”,才能把消息发到队列
    private static final String ROUTING_KEY = "my_key";

    public static void main(String[] args) throws IOException {
        sendMsg("测试消息");
    }

    public static void sendMsg(String message) {
        // 获取连接对象
        Connection conn;
        Channel channel = null;

        // 存储消息内容,方便异步回调时取出
        ConcurrentHashMap<Long, String> map = new ConcurrentHashMap<>();

        // Connection对象位于连接池中,不用关闭,但是需要关闭Channel对象
        try {
            // 获取Connection对象
            conn = RabbitMqUtil.createConn();

            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            channel = RabbitMqUtil.createChannel(conn);

            /**
             * 创建交换机
             * 参数1:交换机名称
             * 参数2:交换机类型;direct代表直接模式
             * 参数3:持久化
             * 参数4:自动删除
             * 参数5:交换机属性
             */
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);

            /**
             * 创建普通队列
             * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:自动删除
             *          没有消费者连接是否自动删除
             * 参数5:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            /**
             * 创建队列和交换机绑定关系
             * 参数1:队列名称
             * 参数2:交换机名称
             * 参数3:绑定键
             */
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY);

            // 开启确认发布,然后才会回调addConfirmListener方法
            channel.confirmSelect();

            //消息发送到交换机成功或者失败,都会回调该方法;注意:如果交换机不存在,程序将会报错,然后停止执行
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    // 成功,删除已发送消息
                    map.remove(deliveryTag);
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("失败:" + map.get(deliveryTag));
                    map.remove(deliveryTag);
                }
            });

            // 无法发送消息到队列,就会调用该方法
            channel.addReturnListener(new ReturnListener() {
                @Override
                public void handleReturn(int replyCode,
                                         String replyText,
                                         String exchange,
                                         String routingKey,
                                         AMQP.BasicProperties properties,
                                         byte[] body) throws IOException {
                    System.out.println("没找到符合的队列而退回的消息 = " + new String(body, "utf-8"));
                }
            });

            /**
             * 发送消息到队列
             * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列
             * 参数1:交换机名称
             *          EXCHANGE_NAME是交换机名称
             *
             * 参数2:路由键名称
             *
             * 参数3:默认值成false,设置为true,将会把找不到队列的消息返还给生产者,然后上面addReturnListener方法才能起到作用
             *
             * 参数3:其他消息属性,目前设置的是消息持久化
             *
             * 参数4,消息内容
             *          消息内容是字节数组,所以我们需要使用getBytes()方法
             */
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));

            // 同步等待,等异步回调跑完,毕竟try里面声明channel,代码执行完毕后将会关闭channel,这样会导致异步回调无法再执行
            channel.waitForConfirms();

            // 打印发送完成信息
            System.out.println("发送消息:" + message);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭Channel对象
            RabbitMqUtil.close(channel);
        }
    }
}

7.5、消费者

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DeliverCallback;
import java.nio.charset.StandardCharsets;

/**
 * 消费者
 *
 * @author 明快de玄米61
 * @date 2022/4/22 23:14
 */
public class Consumer {
    // 交换机名称
    private static final String EXCHANGE_NAME = "full_version_exchange_spring";

    // 队列名称
    private static final String QUEUE_NAME = "full_version_queue_spring";

    // 绑定键名称,指明队列和交换机的绑定关系
    private static final String BINDING_KEY = "my_key";

    public static void main(String[] args) {
        // 消费消息只调用一次,就可以保持持续监听
        consumeMsg();
    }

    public static void consumeMsg() {
        // 获取连接对象
        Connection conn = null;
        Channel channel = null;

        // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel
        try {
            conn = RabbitMqUtil.createConn();
            // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费
            channel = RabbitMqUtil.createChannel(conn);
            /**
             * 创建交换机
             * 参数1:交换机名称
             * 参数2:交换机类型;direct代表直接模式
             * 参数3:持久化
             * 参数4:自动删除
             * 参数5:交换机属性
             */
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, null);

            /**
             * 创建普通队列
             * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息
             * 参数1:队列名称
             * 参数2:是否持久化
             *          队列持久化之后,即使RabbitMQ重启,队列依然存在
             * 参数3:是否排他
             *          设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用
             * 参数4:自动删除
             *          没有消费者连接是否自动删除
             * 参数5:附加参数
             *          比如可以设置死信队列等
             */
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            /**
             * 创建队列和交换机绑定关系
             * 参数1:队列名称
             * 参数2:交换机名称
             * 参数3:绑定键
             */
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY);

            // 消息创建中的回调对象;用来处理从队列异步推送过来的消息
            Channel finalChannel = channel;
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                String str = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println("接收消息:" + str);
                /**
                 * 手动确认消息
                 * 参数1:用来确认消息的发送标签
                 * 参数2:是否确认当前通道中的所有消息
                 *          false(建议):只确认当前标签对应的消息
                 *          true:确认当前通道中的所有未确认消息,这样可能造成部分正在被消费者处理的消息被确认,如果这些消息处理失败了,那么这些消息就丢失了
                 */
                finalChannel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            };
            /**
             * 消费者异步监听队列消息
             * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息
             * 参数1:队列名称;
             *          指定消费者连接的队列
             * 参数2:是否自动确认;
             *        如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息
             *        如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息
             * 参数3:消息传递时的回调对象
             * 参数4:消费者被取消时的回调,暂时没有看懂啥意思
             */
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {
            });
            System.out.println("消费者创建完毕……");
        } catch (Exception e) {
            e.printStackTrace();
            RabbitMqUtil.close(channel);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
回答: RabbitMQ是一个消息队列中间件,它可以被看作是一个服务节点或者服务实例,通常也可以将其视为一台服务器。\[1\]在生产环境中,由于一些不明原因导致RabbitMQ重启,可能会导致消息投递失败和消息丢失。为了确保消息的可靠投递,可以采用事务机制或者使用发送方确认机制。事务机制可以解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被RabbitMQ接收,事务才能提交成功,否则可以行事务回滚和消息重发。然而,使用事务机制会对RabbitMQ的性能产生一定的影响。另一种方法是使用发送方确认机制,通过该机制可以实现消息的可靠投递,即发送方在消息被RabbitMQ确认接收之后才认为消息已经成功投递。\[2\]\[3\]这样可以在RabbitMQ集群不可用的情况下,对无法投递的消息行处理和恢复。 #### 引用[.reference_title] - *1* [Rabbitmq](https://blog.csdn.net/weixin_46634416/article/details/124755747)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [RabbitMQ](https://blog.csdn.net/weixin_73198745/article/details/130910185)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [RabbitMQ入门高级详解(内容有点多)](https://blog.csdn.net/sinat_16658263/article/details/124211232)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值