【Rabbit MQ】快速入门

文章目录

1 MQ 的相关概念

1.1 什么是 MQ

MQ(message queue),从字面意思上看,本质是个队列,队列中存放的内容是 message,是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。

1.2 为什么使用 MQ

(1)流量削峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余。但是在高峰期,如果有两万次下单操作系统是处理不了的,可能会造成服务器宕机。使用消息队列做缓冲后,我们可以把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

(2)应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

(3)异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不用做这些操作。A 服务还能及时的得到异步处理成功的消息。

1.2 MQ 的分类

  1. ActiveMQ
  2. Kafaka:主要用于大数据应用
  3. RocketMQ:出自阿里巴巴的开源产品
  4. RabbitMQ:https://www.rabbitmq.com/news.html

2 RabbitMQ

2.1 RabbitMQ 的概念

RabbitMQ 是一个消息中间件:它接收,存储和转发消息数据。

2.2 四大核心概念

(1)生产者
产生数据发送消息的程序是生产者

(2)交换机
交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推 送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定。

(3)队列
队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式.

(3)消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

2.3 Rabbit 名词解释

在这里插入图片描述
(1)Broker
接收和分发消息的应用,RabbitMQ Server 就是 Message Broker。

(2)Virtual host
出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等。

(3)Connection
publisher/consumer 和 broker 之间的 TCP 连接。

(4)Channel
Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销。

(5)Exchange
message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。

(6)Queue:
消息最终被送到这里等待 consumer 取走。

(7)Binding
exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据。

2.4 安装

(1)官网地址
https://www.rabbitmq.com/download.html
778

(2)上传安装包到 /opt/software 目录下

-rw-rw-r--. 1 liyibin liyibin  18850824 1114 20:43 erlang-21.3-1.el7.x86_64.rpm
-rw-rw-r--. 1 liyibin liyibin  15520399 1114 20:43 rabbitmq-server-3.8.8-1.el7.noarch.rpm

(3)安装文件(分别按照以下顺序安装)

  1. rpm -ivh erlang-21.3-1.el7.x86_64.rpm 安装依赖环境
  2. yum install socat -y
  3. rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm

(4)常用命令

  1. 添加开机启动 RabbitMQ 服务 chkconfig rabbitmq-server on
  2. 启动服务 /sbin/service rabbitmq-server start
  3. 查看服务状态 /sbin/service rabbitmq-server status
  4. 停止服务(选择执行) /sbin/service rabbitmq-server stop
  5. 开启 web 管理插件 rabbitmq-plugins enable rabbitmq_management
    web 端默认访问端口 15672

(5)添加新用户

  1. 创建账号 rabbitmqctl add_user admin 123456
  2. 设置用户角色 rabbitmqctl set_user_tags admin administrator
  3. 设置用户权限
# set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
# 用户 user_admin 具有/ 这个 virtual host 中所有资源的配置、写、读权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
  1. 查看当前用户和角色 rabbitmqctl list_users
  2. 使用 admin 用户登录
    在这里插入图片描述

(6)其它命令

  1. 关闭应用的命令 rabbitmqctl stop_app
  2. 清除的命令 rabbitmqctl reset
  3. 重新启动命令 rabbitmqctl start_app

2 Hellow World

在本教程的这一部分中,我们将用 Java 编写两个程序。发送单个消息的生产者和接收消息并打印出来的消费者。我们将介绍 Java API 中的一些细节。

在下图中,“ P”是我们的生产者,“ C”是我们的消费者。中间的框是一个队列-RabbitMQ 代表使用者保留的消息缓冲区。
在这里插入图片描述

2.1 新建项目

导入以下依赖

<!--指定 jdk 编译版本-->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>8</source>
                <target>8</target>
            </configuration>
        </plugin>
    </plugins>
</build>
<dependencies>
    <!--rabbitmq 依赖客户端-->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.8.0</version>
    </dependency>
    <!--操作文件流的一个依赖-->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
</dependencies>

2.2 消息生产者

public class Producer {

    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("hadoop102");
        factory.setUsername("admin");
        factory.setPassword("123456");

        try (
                // 2. 创建一个连接
                Connection connection = factory.newConnection();
                // 3. 新建一个通道
                Channel channel = connection.createChannel()
        ) {
            // 4. 创建一个 队列
            //  1.队列名称
            //  2.队列里面的消息是否持久化 默认消息存储在内存中
            //  3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
            //  4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
            //  5.其他参数
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);

            // 5. 发送消息
            //  1.发送到那个交换机
            //  2.路由的 key 是哪个
            //  3.其他的参数信息
            //  4.发送消息的消息体
            channel.basicPublish("", QUEUE_NAME, null, "hello world".getBytes());

            System.out.println("消息发送成功");
        }
    }
}

2.3 消息消费者

public class Consumer {

    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("hadoop102");
        factory.setUsername("admin");
        factory.setPassword("123456");

        try (
                // 2. 创建一个连接
                Connection connection = factory.newConnection();
                // 3. 新建一个通道
                Channel channel = connection.createChannel()
        ) {
            System.out.println("等待接收消息");

            // 4. 消费者消费消息
            //  1.消费哪个队列
            //  2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
            //  3. 推送的消息如何进行消费的接口回调
            //  4.消费者未成功消费的回调
            channel.basicConsume(QUEUE_NAME, true,
                    (s, delivery) -> {
                        System.out.println(new String(delivery.getBody()));
                    },
                    s -> {
                        System.out.println("消息消费被中断");
                    });
        }
    }
}

2.4 测试结果

生产者:
在这里插入图片描述
消费者:
在这里插入图片描述

3 Work Queues

工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
在这里插入图片描述

3.1 轮询消费消息

3.1.1 工具类

public class RabbitMqUtils {

    public static Channel getChannel() throws Exception {
        // 1. 连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("hadoop102");
        factory.setUsername("admin");
        factory.setPassword("123456");

        // 2. 创建一个连接
        Connection connection = factory.newConnection();
        // 3. 新建一个通道
       return connection.createChannel();
    }
}

3.1.2 生产者

public class Producer {

    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        final Channel channel = RabbitMqUtils.getChannel();

        final Scanner scanner = new Scanner(System.in);

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        while (scanner.hasNextLine()) {
            final String message = scanner.nextLine();
            // 发送消息
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        }
    }
}

3.1.3 消费者

public class Consumer {

    public static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        final Channel channel = RabbitMqUtils.getChannel();

        System.out.println("等待接收消息");

        // 4. 消费者消费消息
        //  1.消费哪个队列
        //  2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
        //  3. 推送的消息如何进行消费的接口回调
        //  4.消费者未成功消费的回调
        channel.basicConsume(QUEUE_NAME, true,
                (s, delivery) -> {
                    System.out.println(new String(delivery.getBody()));
                },
                s -> {
                    System.out.println("消息消费被中断");
                });
    }
}

3.1.4 测试

  1. 启动两个消费者线程
    在这里插入图片描述
  2. 启动生产者
    在这里插入图片描述
  3. 发送消息
    在这里插入图片描述
    消费者1
    在这里插入图片描述
    消费者2
    在这里插入图片描述

3.2 消息应答

3.2.1 概念

假如RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。

为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。

3.2.2 自动应答

消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用

3.2.3 消息应答的方法

(1)Channel.basicAck(用于肯定确认):RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了。
(2)Channel.basicNack(用于否定确认)
(3)Channel.basicReject(用于否定确认):与 Channel.basicNack 相比少一个参数不处理该消息了直接拒绝,可以将其丢弃了。

3.2.4 消息自动重新入队

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。

3.2.5 消息手动应答代码

消息生产者

public class Producer {

    public static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);

            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入信息:");
            while (scanner.hasNextLine()) {
                final String message = scanner.nextLine();
                // 发送消息
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
                System.out.println("生产者发出消息:" + message);
            }
        }
    }
}

消费者1

public class Consumer1 {

    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1 等待接收消息处理时间较短");

        // 手动应答
        channel.basicConsume(QUEUE_NAME, false,
                (consumerTag, message) -> {
                    String msg = new String(message.getBody());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignore) {
                    }

                    System.out.println("接收到消息:" + msg);

                    // 1. 消息标记 tag
                    // 2. 是否批量应答
                    channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
                },
                consumerTag -> {
                    System.out.println(consumerTag + " 消费者取消消费接口回调逻辑");
                });
    }
}

消费者2

public class Consumer2 {

    private static final String QUEUE_NAME = "ack_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2 等待接收消息处理时间较长");

        // 手动应答
        channel.basicConsume(QUEUE_NAME, false,
                (consumerTag, message) -> {
                    String msg = new String(message.getBody());
                    try {
                        Thread.sleep(30000);
                    } catch (InterruptedException ignore) {
                    }

                    System.out.println("接收到消息:" + msg);

                    // 1. 消息标记 tag
                    // 2. 是否批量应答
                    channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
                },
                consumerTag -> {
                    System.out.println(consumerTag + " 消费者取消消费接口回调逻辑");
                });
    }
}

3.2.6 手动应答效果演示

在这里插入图片描述
在这里插入图片描述

3.3 RabbitMQ 持久化

3.3.1 概念

刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化

3.3.2 队列持久化

之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化。
在这里插入图片描述
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。

以下为web页面中非持久化和持久化队列的 UI 显示区别。
在这里插入图片描述
在这里插入图片描述

3.3.3 消息持久化

要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN添加这个属性。
在这里插入图片描述
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。

3.3.4 不公平分发

默认情况下 RabbitMQ 分发消息采用的轮询分发,但是在某些场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候还是采用轮询分发,就会导致处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直繁忙。

为了避免这种情况,我们可以在消费者端设置参数 channel.basicQos(1);
在这里插入图片描述
在这里插入图片描述
该值的意思是消费者目前只能处理一个任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 worker 或者改变其他存储任务的策略。

3.3.5 预取值

由于本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息。另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basicQos方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认。

例如:
在这里插入图片描述

4 发布确认

4.1 发布确认原理

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的 multiple = true表示到这个序列号之前的所有消息都已经得到了处理

confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

4.2 发布确认的策略

4.2.1 开启发布确认的方法

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法。
在这里插入图片描述

4.2.2 单个发布确认

这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirms 这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

public static void publishMessagesIndividually() throws Exception {
    try (Channel channel = RabbitMqUtils.getChannel()) {
        String queueName = UUID.randomUUID().toString();
        // 队列持久化
        channel.queueDeclare(queueName, true, false, false, null);
        // 确认发布
        channel.confirmSelect();

        long begin = System.currentTimeMillis();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + "";
            channel.basicPublish("", queueName,
                    MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

            // 服务端返回 false 或超时时间内未返回,生产者可以消息重发
            boolean flag = channel.waitForConfirms();
            if (flag) {
                System.out.println("消息发送成功");
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
    }
}

4.2.3 批量确认发布

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。

public static void publishMessagesInBatch() throws Exception {
    try (Channel channel = RabbitMqUtils.getChannel()) {
        String queueName = UUID.randomUUID().toString();
        // 队列持久化
        channel.queueDeclare(queueName, true, false, false, null);
        // 确认发布
        channel.confirmSelect();

        // 批量确认消息大小
        int batchSize = 100;
        //未确认消息个数
        int currentCount = 0;
        long begin = System.currentTimeMillis();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + "";
            channel.basicPublish("", queueName, null, message.getBytes());
            currentCount++;

            if (currentCount == batchSize) {
                channel.waitForConfirms();
                currentCount = 0;
            }
        }

        //为了确保还有剩余没有确认消息 再次确认
        if (currentCount > 0) {
            channel.waitForConfirms();
        }
        long end = System.currentTimeMillis();
        System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms");
    }
}

4.2.4 异步确认发布

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。

public static void publishMessageAsync() throws Exception {
   try (Channel channel = RabbitMqUtils.getChannel()) {
       String queueName = UUID.randomUUID().toString();
       channel.queueDeclare(queueName, false, false, false, null);
       //开启发布确认
       channel.confirmSelect();

       // 线程安全有序的一个哈希表,适用于高并发的情况
       //  1.轻松的将序号与消息进行关联
       //  2.轻松批量删除条目 只要给到序列号
       //  3.支持并发访问
       ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();

       // 添加一个异步确认的监听器
       //  1.确认收到消息的回调
       //  2.未收到消息的回调
       channel.addConfirmListener(
               (sequenceNumber, multiple) -> {
                   if (multiple) {
                       //返回的是小于等于当前序列号的未确认消息 是一个 map
                       ConcurrentNavigableMap<Long, String> confirmed =
                               outstandingConfirms.headMap(sequenceNumber, true);
                       //清除该部分未确认消息
                       confirmed.clear();
                   } else {
                       //只清除当前序列号的消息
                       outstandingConfirms.remove(sequenceNumber);
                   }
               },
               (sequenceNumber, multiple) -> {
                   String message = outstandingConfirms.get(sequenceNumber);
                   System.out.println("发布的消息" + message + "未被确认,序列号" + sequenceNumber);
               }
       );

       long begin = System.currentTimeMillis();
       for (int i = 0; i < MESSAGE_COUNT; i++) {
           String message = "消息" + i;
           // channel.getNextPublishSeqNo()获取下一个消息的序列号
           // 通过序列号与消息体进行一个关联
           // 全部都是未确认的消息体
           outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
           channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
       }

       long end = System.currentTimeMillis();
       System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms");
   }
}

4.2.5 如何处理异步未确认消息

最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

4.2.6 3 种发布确认速度对比

public static void main(String[] args) throws Exception {
    // 单个确认发布
    publishMessagesIndividually();
    // 批量确认发布
    publishMessagesInBatch();
    // 异步确认发布
    publishMessageAsync();
}

在这里插入图片描述

(1)单独发布消息:同步等待确认,简单,但吞吐量非常有限。
(2)批量发布消息:批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
(3)异步处理:最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些。

5 交换机

在本节中,我们将做一些完全不同的事情-我们将消息传达给多个消费者。这种模式称为 ”发布/订阅”。

为了说明这种模式,我们将构建一个简单的日志系统。它将由两个程序组成:第一个程序将发出日志消息,第二个程序是消费者。其中我们会启动两个消费者,获取不同的队列中的消息。

5.1 Exchanges

5.1.1 概念

RabbitMQ 消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。

相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。

5.1.2 Exchanges 的类型

总共有以下类型:直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)。

5.1.3 无名 Exchange

在前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换机,我们通过空字符串("")进行标识。
在这里插入图片描述

5.2 临时队列

每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。

创建临时队列的方式如下:

channel.queueDeclare().getQueue();

临时队列在 web 页面上:
在这里插入图片描述

5.3 绑定(bindings)

binding 是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定。
在这里插入图片描述

5.4 Fanout

5.4.1 Fanout 介绍

Fanout 这种类型非常简单。它是将接收到的所有消息广播到它知道的所有队列中。

系统中默认 exchange:
在这里插入图片描述

5.4.2 Fanout 实例

在这里插入图片描述
LogProducer 生产日志消息:

public class LogProducer {

    /**
     * 交换机名称
     */
    private static final String EXCHANGE_NAME = "logs";
    /**
     * 队列名称
     */
    private static final String QUEUE1 = "logQueue1";
    private static final String QUEUE2 = "logQueue2";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 1. 声明交换机
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

            // 2. 声明队列
            channel.queueDeclare(QUEUE1, false, false, false, null);
            channel.queueDeclare(QUEUE2, false, false, false, null);

            // 3. 绑定交换机和队列
            channel.queueBind(QUEUE1, EXCHANGE_NAME, "");
            channel.queueBind(QUEUE2, EXCHANGE_NAME, "");

            System.out.println("请输入消息:");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String message = scanner.nextLine();

                // 4. 发送消息测试
                channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
            }
        }
    }
}

LogConsumer1 消费 logQueue1 的消息:

public class LogConsumer1 {

    private static final String QUEUE1 = "logQueue1";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("LogConsumer1 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE1, true,
                (consumerTag, message) ->
                        System.out.println("LogConsumer1 收到消息:" + new String(message.getBody())),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

LogConsumer2 消费 logQueue2 的消息:

public class LogConsumer2 {

    private static final String QUEUE2 = "logQueue2";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("LogConsumer2 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE2, true,
                (consumerTag, message) ->
                        System.out.println("LogConsumer2 收到消息:" + new String(message.getBody())),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

5.4.3 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.5 Direct

5.5.1 Direct exchange 介绍

上一节中的我们的日志系统将所有消息广播给所有消费者,对此我们想做一些改变,例如我们希望将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。Fanout 这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的 routingKey 队列中去。
在这里插入图片描述

5.5.2 多重绑定

如果 exchange 的绑定类型是 direct,但是它绑定的多个队列的 key 如果都相同,在这种况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了,就跟广播差不多,如图所示。
在这里插入图片描述

5.5.3 实例

在这里插入图片描述
DirectLogProducer

public class DirectLogProducer {

    /**
     * 交换机名称
     */
    private static final String EXCHANGE_NAME = "directLogs";
    /**
     * 队列名称
     */
    private static final String QUEUE1 = "directLogQueue1";
    private static final String QUEUE2 = "directLogQueue2";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 1. 声明交换机
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

            // 2. 声明队列
            channel.queueDeclare(QUEUE1, false, false, false, null);
            channel.queueDeclare(QUEUE2, false, false, false, null);

            // 3. 绑定交换机和队列
            channel.queueBind(QUEUE1, EXCHANGE_NAME, "error");
            channel.queueBind(QUEUE2, EXCHANGE_NAME, "info");
            channel.queueBind(QUEUE2, EXCHANGE_NAME, "warning");

            //创建多个 bindingKey
            Map<String, String> bindingKeyMap = new HashMap<>();
            bindingKeyMap.put("info","普通 info 信息");
            bindingKeyMap.put("warning","警告 warning 信息");
            bindingKeyMap.put("error","错误 error 信息");
            //debug 没有消费这接收这个消息 所有就丢失了
            bindingKeyMap.put("debug","调试 debug 信息");

            for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
                String routingKey = entry.getKey();
                String message = entry.getValue();

                // 4. 发送消息测试
                channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes(StandardCharsets.UTF_8));
            }
        }
    }
}

DirectLogConsumer1

public class DirectLogConsumer1 {

    private static final String QUEUE1 = "directLogQueue1";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("LogConsumer1 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE1, true,
                (consumerTag, message) ->
                        System.out.println("DirectLogConsumer1 收到消息:message="
                                + new String(message.getBody())
                                + ", routingKey="
                                + message.getEnvelope().getRoutingKey()),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

DirectLogConsumer2

public class DirectLogConsumer2 {

    private static final String QUEUE1 = "directLogQueue2";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("LogConsumer1 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE1, true,
                (consumerTag, message) ->
                        System.out.println("DirectLogConsumer2 收到消息:message="
                                + new String(message.getBody())
                                + ", routingKey="
                                + message.getEnvelope().getRoutingKey()),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

5.5.4 测试

在这里插入图片描述
在这里插入图片描述

5.6 Topics

尽管使用 direct 交换机可以选择性接收队列的消息,但是它仍然存在局限性-比方说我们想接收的日志类型有 info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候 direct 就办不到了。这个时候就只能使用 topic 类型。

5.6.1 Topic 的要求

发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”.这种类型的。这个单词列表最多不能超过 255 个字节。

特殊匹配规则
(1)* 可以代替一个单词
(2)# 可以替代零个或多个单词

5.6.2 匹配案例

在这里插入图片描述
topicLogQueue1–>绑定的是:
(1)中间带 orange 带 3 个单词的字符串(.orange.)

topicLogQueue2–>绑定的是:
(1)最后一个单词是 rabbit 的 3 个单词(..rabbit)
(2)第一个单词是 lazy 的多个单词(lazy.#)

下面是不同 routingKey,队列的接收情况:

(1)quick.orange.rabbit 被队列 topicLogQueue1 topicLogQueue2 接收到。
(2)lazy.orange.elephant 被队列 topicLogQueue1 topicLogQueue2 接收到。
(3)quick.orange.fox 被队列 topicLogQueue1 接收到。
(4)lazy.brown.fox 被队列 topicLogQueue2 接收到。
(5)lazy.pink.rabbit 虽然满足两个绑定但只被队列 topicLogQueue2 接收一次。
(6)quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃。
(7)quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃。
(8)lazy.orange.male.rabbit 是四个单词但匹配 topicLogQueue2。

注意
(1)当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了。
(2)如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了。

5.6.3 编码实现

TopicLogProducer

public class TopicLogProducer {

    /**
     * 交换机名称
     */
    private static final String EXCHANGE_NAME = "topicLogs";
    /**
     * 队列名称
     */
    private static final String QUEUE1 = "topicLogQueue1";
    private static final String QUEUE2 = "topicLogQueue2";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 1. 声明交换机
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            // 2. 声明队列
            channel.queueDeclare(QUEUE1, false, false, false, null);
            channel.queueDeclare(QUEUE2, false, false, false, null);

            // 3. 绑定交换机和队列
            channel.queueBind(QUEUE1, EXCHANGE_NAME, "*.orange.*");
            channel.queueBind(QUEUE2, EXCHANGE_NAME, "*.*.rabbit");
            channel.queueBind(QUEUE2, EXCHANGE_NAME, "lazy.#");

            //创建多个 bindingKey
            Map<String, String> bindingKeyMap = new HashMap<>();
            bindingKeyMap.put("quick.orange.rabbit", "被队列 topicLogQueue1 topicLogQueue2 接收到");
            bindingKeyMap.put("lazy.orange.elephant", "被队列 topicLogQueue1 topicLogQueue2 接收到");
            bindingKeyMap.put("quick.orange.fox", "被队列 topicLogQueue1 接收到");
            bindingKeyMap.put("lazy.brown.fox", "被队列 topicLogQueue2 接收到");
            bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 topicLogQueue2 接收一次");
            bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
            bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
            bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 topicLogQueue2");

            for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
                String routingKey = entry.getKey();
                String message = entry.getValue();

                // 4. 发送消息测试
                channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes(StandardCharsets.UTF_8));
            }
        }
    }
}

TopicLogConsumer1

public class TopicLogConsumer1 {

    private static final String QUEUE1 = "topicLogQueue1";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("TopicLogConsumer1 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE1, true,
                (consumerTag, message) ->
                        System.out.println("TopicLogConsumer1 收到消息:message="
                                + new String(message.getBody())
                                + ", routingKey="
                                + message.getEnvelope().getRoutingKey()),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

TopicConsumer2

public class TopicLogConsumer2 {

    private static final String QUEUE1 = "topicLogQueue2";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("TopicLogConsumer2 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE1, true,
                (consumerTag, message) ->
                        System.out.println("TopicLogConsumer2 收到消息:message="
                                + new String(message.getBody())
                                + ", routingKey="
                                + message.getEnvelope().getRoutingKey()),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

5.6.4 测试结果

在这里插入图片描述
在这里插入图片描述

6 死信队列

6.1 概念

死信,顾名思义就是无法被消费的消息。一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:

  1. 为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。
  2. 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。

6.2 死信的来源

  1. 消息 TTL 过期。
  2. 队列达到最大长度(队列满了,无法再添加数据到 mq 中)。
  3. 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false。

6.3 死信实例

6.3.1 结构图

在这里插入图片描述

6.3.2 消息 TTL 过期

生产者代码

public class DeadLetterProducer1 {

    private static final String NORMAL_EXCHANGE = "normal_exchange";
    private static final String NORMAL_QUEUE = "normal_queue";
    private static final String NORMAL_KEY = "normal_key";

    private static final String DEAD_EXCHANGE = "dead_exchange";
    private static final String DEAD_QUEUE = "dead_queue";
    private static final String DEAD_KEY = "dead_key";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 1. 声明交换机
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

            // 2. 声明队列
            Map<String, Object> arguments = new HashMap<>();
            // 设置死信交换机
            arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
            arguments.put("x-dead-letter-routing-key", DEAD_KEY);
            channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);

            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);

            // 3. 绑定交换机和队列
            channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_KEY);
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_KEY);

            //设置消息的 TTL 时间
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();

            //该信息是用作演示队列个数限制
            for (int i = 1; i < 11; i++) {
                String message = "info" + i;
                channel.basicPublish(NORMAL_EXCHANGE, NORMAL_KEY, properties, message.getBytes());

                System.out.println("生产者发送消息:" + message);
            }
        }
    }
}

消费者代码

public class DeadLetterConsumer {

    private static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("DeadLetterConsumer 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                DEAD_QUEUE, true,
                (consumerTag, message) ->
                        System.out.println("DeadLetterConsumer 收到消息:message="
                                + new String(message.getBody())
                                + ", routingKey="
                                + message.getEnvelope().getRoutingKey()),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

启动生产者:

生产者发生了10条消息,正常队列中有10条未消费的消息
在这里插入图片描述
10 秒后正常正常队列中的消息没有被消费,消息进入死信队列
在这里插入图片描述
启动消费者:
死信队列的消息被消费。
在这里插入图片描述
在这里插入图片描述

6.3.3 队列满了

生产者代码

public class DeadLetterProducer2 {

    private static final String NORMAL_EXCHANGE = "normal_exchange";
    private static final String NORMAL_QUEUE = "normal_queue";
    private static final String NORMAL_KEY = "normal_key";

    private static final String DEAD_EXCHANGE = "dead_exchange";
    private static final String DEAD_QUEUE = "dead_queue";
    private static final String DEAD_KEY = "dead_key";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 1. 声明交换机
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

            // 2. 声明队列
            Map<String, Object> arguments = new HashMap<>();
            // 设置死信交换机
            arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
            arguments.put("x-dead-letter-routing-key", DEAD_KEY);
            arguments.put("x-max-length", 6);
            channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);

            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);

            // 3. 绑定交换机和队列
            channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_KEY);
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_KEY);

            //该信息是用作演示队列个数限制
            for (int i = 1; i < 11; i++) {
                String message = "info" + i;
                channel.basicPublish(NORMAL_EXCHANGE, NORMAL_KEY, null, message.getBytes());

                System.out.println("生产者发送消息:" + message);
            }
        }
    }
}

消费者代码:不需要修改

启动生产者:
在这里插入图片描述
启动消费者:
在这里插入图片描述
在这里插入图片描述

6.3.4 消息被拒

生产者代码:

public class DeadLetterProducer3 {

    private static final String NORMAL_EXCHANGE = "normal_exchange";
    private static final String NORMAL_QUEUE = "normal_queue";
    private static final String NORMAL_KEY = "normal_key";

    private static final String DEAD_EXCHANGE = "dead_exchange";
    private static final String DEAD_QUEUE = "dead_queue";
    private static final String DEAD_KEY = "dead_key";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            // 1. 声明交换机
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

            // 2. 声明队列
            Map<String, Object> arguments = new HashMap<>();
            // 设置死信交换机
            arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
            arguments.put("x-dead-letter-routing-key", DEAD_KEY);
            channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);

            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);

            // 3. 绑定交换机和队列
            channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_KEY);
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_KEY);

            //该信息是用作演示队列个数限制
            for (int i = 1; i < 11; i++) {
                String message = "info" + i;
                channel.basicPublish(NORMAL_EXCHANGE, NORMAL_KEY, null, message.getBytes());

                System.out.println("生产者发送消息:" + message);
            }
        }
    }
}

消费者代码:需要手动应答

public class NormalQueueConsumer {

    private static final String NORMAL_QUEUE = "normal_queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("NormalQueueConsumer 等待接收消息...");

        // 接收消息 - 自动应答不开启
        channel.basicConsume(
                NORMAL_QUEUE, false,
                (consumerTag, message) -> {
                    String msg = new String(message.getBody());
                    if ("info5".equals(msg)) {
                        // 拒绝消息
                        channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
                        System.out.println("NormalQueueConsumer 拒绝消息,message = " + msg);
                    } else {
                        channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
                        System.out.println("NormalQueueConsumer 接收消息,message = " + msg);
                    }
                 },
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

启动生产者:
在这里插入图片描述
启动正常队列消费者:
在这里插入图片描述
在这里插入图片描述

7 延时队列

7.1 延时队列概念

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

7.2 延时队列应用场景

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。

7.3 RabbitMQ 中的 TTL

TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。

7.3.1 消息设置 TTL

在这里插入图片描述

7.3.2 队列设置 TTL

在这里插入图片描述

7.4 整合 sprintboot

7.4.1 创建 MAVEN 项目导入依赖:

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-amqp</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework.amqp</groupId>
		<artifactId>spring-rabbit-test</artifactId>
		<scope>test</scope>
	</dependency>

	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
	</dependency>
</dependencies>

7.4.2 修改配置文件

spring:
    rabbitmq:
        host: hadoop102
        port: 5672
        username: admin
        password: 123456

7.5 队列 TTL

7.5.1 代码结构图

在这里插入图片描述

7.5.2 RabbitMQ 配置类

@Configuration
public class DeadLetterQueueConfig {

    public static final String EXCHANGE_X = "X";
    public static final String QUEUE_A = "QA";
    public static final String ROUTING_KEY_XA = "XA";
    public static final String QUEUE_B = "QB";
    public static final String ROUTING_KEY_XB = "XB";

    public static final String EXCHANGE_Y = "Y";
    public static final String QUEUE_D = "QD";
    public static final String ROUTING_KEY_YD = "YD";


    /**
     * 交换机
     */
    @Bean
    public DirectExchange exchangeX() {
        return new DirectExchange(EXCHANGE_X);
    }
    /**
     * 队列配置
     */
    @Bean
    public Queue queueA() {
        Map<String, Object> args = new HashMap<>();
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", EXCHANGE_Y);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", ROUTING_KEY_YD);
        //声明队列的 TTL
        args.put("x-message-ttl", 10000);

        return QueueBuilder.durable(QUEUE_A)
                .withArguments(args)
                .build();
    }
    @Bean
    public Binding bindingQaToX(DirectExchange exchangeX, Queue queueA) {
        return BindingBuilder.bind(queueA).to(exchangeX).with(ROUTING_KEY_XA);
    }
    
    @Bean
    public Queue queueB() {
        Map<String, Object> args = new HashMap<>();
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", EXCHANGE_Y);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", ROUTING_KEY_YD);
        //声明队列的 TTL
        args.put("x-message-ttl", 40000);

        return QueueBuilder.durable(QUEUE_B)
                .withArguments(args)
                .build();
    }
    @Bean
    public Binding bindingQbToX(DirectExchange exchangeX, Queue queueB) {
        return BindingBuilder.bind(queueB).to(exchangeX).with(ROUTING_KEY_XB);
    }

    @Bean
    public DirectExchange exchangeY() {
        return new DirectExchange(EXCHANGE_Y);
    }
    @Bean
    public Queue queueD() {
        return new Queue(QUEUE_D);
    }
    @Bean
    public Binding bindingQdToY(DirectExchange exchangeY, Queue queueD) {
        return BindingBuilder.bind(queueD).to(exchangeY).with(ROUTING_KEY_YD);
    }
}

7.5.3 消息生产者代码

@Slf4j
@RequestMapping("/rabbitmq")
@RestController
public class RabbitmqController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsg/{msg}")
    public void queueTtl(@PathVariable String msg) {
        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), msg);
        rabbitTemplate.convertAndSend(DeadLetterConfig.EXCHANGE_X, DeadLetterConfig.ROUTING_KEY_XA, "ttl为10秒的消息:" + msg);
        rabbitTemplate.convertAndSend(DeadLetterConfig.EXCHANGE_X, DeadLetterConfig.ROUTING_KEY_XB, "ttl为40秒的消息:" + msg);
    }
}

7.5.4 消息消费者代码

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    @RabbitListener(queues = DeadLetterConfig.QUEUE_D)
    public void receiveFromQueueD(Message message, Channel channel) {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
    }
}

7.5.5 测试

启动 springboot 后,发起一个请求 http://localhost:8080/rabbitmq/sendMsg1/hello world
在这里插入图片描述
第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。

7.6 消息 TTL

但是为队列设置 ttl 时间有局限性,每次有新的延时时间就要新增一个队列,这是不实际的。因此可以为每一个消息设置 ttl。

7.6.1 代码结构图

在这里新增了一个队列 QC,该队列不设置 TTL 时间。
在这里插入图片描述

7.6.2 配置类代码

增加如下配置

public static final String QUEUE_C = "QC";
public static final String ROUTING_KEY_XC = "XC";

@Bean
public Queue queueC() {
    Map<String, Object> args = new HashMap<>();
    //声明当前队列绑定的死信交换机
    args.put("x-dead-letter-exchange", EXCHANGE_Y);
    //声明当前队列的死信路由 key
    args.put("x-dead-letter-routing-key", ROUTING_KEY_YD);

    return QueueBuilder.durable(QUEUE_C)
            .withArguments(args)
            .build();
}
@Bean
public Binding bindingQcToX(DirectExchange exchangeX, Queue queueC) {
    return BindingBuilder.bind(queueC).to(exchangeX).with(ROUTING_KEY_XC);
}

7.6.3 消息生产者代码

 @GetMapping("/sendMsg2/{msg}/{ttl}")
 public void queueTtl(@PathVariable String msg, @PathVariable String ttl) {
     rabbitTemplate.convertAndSend(
             DeadLetterQueueConfig.EXCHANGE_X,
             DeadLetterQueueConfig.ROUTING_KEY_XC,
             msg,
             correlationData -> {
                 correlationData.getMessageProperties().setExpiration(ttl);
                 return correlationData;
             });

     log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 QC:{}", new Date(), ttl, msg);
 }

7.6.4 测试

发起请求
http://localhost:8080/rabbitmq/sendMsg2/你好1/20000
http://localhost:8080/rabbitmq/sendMsg2/你好2/2000

在这里插入图片描述
由于 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

7.7 Rabbitmq 插件实现延时队列

如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。

7.7.1 安装延时队列插件

(1)在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
(2)进入 RabbitMQ 的安装目录下的 plgins 目录:/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
(3)执行下面命令让该插件生效,:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
(4)然后重启 RabbitMQ:service rabbitmq-server restart

在这里插入图片描述

7.7.2 代码结构图

在这里插入图片描述

7.7.3 配置类代码

在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制,消息传递后并不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。

@Configuration
public class DelayedMessageQueueConfig {

    public static final String EXCHANGE_DELAYED = "delayed_exchange";
    public static final String QUEUE_DELAYED = "delayed_queue";
    public static final String ROUTING_KEY_DELAYED = "delayed_routing_key";

    /**
     * 自定义类型的交换机
     */
    @Bean
    public CustomExchange exchangeDelayed() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(EXCHANGE_DELAYED,
                "x-delayed-message", true, false, args);
    }

    /**
     * 队列配置
     */
    @Bean
    public Queue queueDelayed() {
        return new Queue(QUEUE_DELAYED);
    }

    @Bean
    public Binding bindingDelayedQueueToDelayedExchange(CustomExchange exchangeDelayed, Queue queueDelayed) {
        return BindingBuilder.bind(queueDelayed).to(exchangeDelayed).with(ROUTING_KEY_DELAYED).noargs();
    }
}

7.7.4 消息生产者代码

@GetMapping("/sendMsg3/{msg}/{delayTime}")
public void queueDelayedMessage(@PathVariable String msg, @PathVariable Integer delayTime) {
    rabbitTemplate.convertAndSend(
            DelayedMessageQueueConfig.EXCHANGE_DELAYED,
            DelayedMessageQueueConfig.ROUTING_KEY_DELAYED,
            msg,
            correlationData -> {
                correlationData.getMessageProperties().setDelay(delayTime);
                return correlationData;
            });

    log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}", new Date(), delayTime, msg);
}

7.7.5 消息消费者代码

@Slf4j
@Component
public class DelayedMessageQueueConsumer {

    @RabbitListener(queues = DelayedMessageQueueConfig.QUEUE_DELAYED)
    public void receiveFromQueueD(@Headers Map<String, Object> headers, @Payload String message) {
        log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), message);
    }
}

7.7.6 测试

发起请求:
http://localhost:8080/rabbitmq/sendMsg3/come on baby1/20000
http://localhost:8080/rabbitmq/sendMsg3/come on baby2/2000
在这里插入图片描述
第二个消息被先消费掉了,符合预期。

8 发布确认高级

在生产环境中由于一些不明原因,导致 RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢?

8.1 确认发布

8.1.1 确认机制方案

在这里插入图片描述

8.1.2 代码结构图

在这里插入图片描述

8.1.3 配置文件

配置文件中添加

spring:
    rabbitmq:
        publisher-confirm-type: correlated

可选值:
(1)NONE:禁用发布确认模式,是默认值
(2)CORRELATED:发布消息成功到交换器后会触发回调方法
(3)SIMPLE:需使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑

8.1.4 配置类

@Configuration
public class ConfirmConfig {

    public static final String EXCHANGE_CONFIRM = "confirm.exchange";
    public static final String QUEUE_CONFIRM = "confirm.queue";
    public static final String ROUTING_KEY_CONFIRM = "confirm.routing.key";

    @Bean
    public DirectExchange exchangeConfirm() {
        return new DirectExchange(EXCHANGE_CONFIRM);
    }
    @Bean
    public Queue queueConfirm() {
        return QueueBuilder.durable(QUEUE_CONFIRM).build();
    }
    @Bean
    public Binding bingingConfirmQueueToConfirmExchange(DirectExchange exchangeConfirm, Queue queueConfirm) {
        return BindingBuilder.bind(queueConfirm).to(exchangeConfirm).with(ROUTING_KEY_CONFIRM);
    }
}

8.1.5 生产者

@Autowired
private ConfirmCallback confirmCallback;

@PostConstruct
public void init() {
    rabbitTemplate.setConfirmCallback(confirmCallback);
}


@GetMapping("/sendMsg4/{msg}")
public void sendMsgToConfirmQueue(@PathVariable String msg) {
    // 该数据会有服务端的交换机返回一个原样的
    CorrelationData data1 = new CorrelationData();
    data1.setId("1");
    rabbitTemplate.convertAndSend(ConfirmConfig.EXCHANGE_CONFIRM, ConfirmConfig.ROUTING_KEY_CONFIRM, msg, data1);

    // 发生数据给一个错误的交换机
    CorrelationData data2 = new CorrelationData();
    data2.setId("2");
    rabbitTemplate.convertAndSend(ConfirmConfig.EXCHANGE_CONFIRM + "2", ConfirmConfig.ROUTING_KEY_CONFIRM, msg, data2);

    log.info("当前时间:{}, 发送 {}", new Date(), msg);
}

8.1.6 确认回调接口

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

    /**
     * 交换机不管是否收到消息的一个回调方法
     *
     * @param correlationData 消息相关数据 生产者发生的
     * @param ack             是否已收到
     * @param cause           原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("交换机已经收到 id 为: {} 的消息", correlationData.getId());
        } else {
            log.info("交换机还未收到 id 为: {} 消息,由于原因: {}", correlationData.getId(), cause);
        }
    }
}

8.1.7 消费者

@Slf4j
@Component
public class ConfirmConsumer {

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

8.1.8 测试

首先发送数据给错误的交换机测试:
在这里插入图片描述
测试结果:
在这里插入图片描述
未找到交换机。

再次测试发送数据给错误的路由:
在这里插入图片描述
测试结果:
在这里插入图片描述
可以看到交换机都发送了确认应答,当时还是由消息丢失了。因为一旦交换机收到了消息就会回复确认消息,不管消息有没有正确的进入到队列中。

8.2 回退消息

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。

我们可以通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

8.1.1

配置文件
配置文件中添加

spring:
    rabbitmq:
        publisher-returns: true

8.1.2 回调接口

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

    /**
     * 交换机不管是否收到消息的一个回调方法
     *
     * @param correlationData 消息相关数据 生产者发生的
     * @param ack             是否已收到
     * @param cause           原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("交换机已经收到 id 为: {} 的消息", correlationData.getId());
        } else {
            log.info("交换机还未收到 id 为: {} 消息,由于原因: {}", correlationData.getId(), cause);
        }
    }
	
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        Message message = returned.getMessage();
        String exchange = returned.getExchange();
        String replyText = returned.getReplyText();
        String routingKey = returned.getRoutingKey();

        log.error("消息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}", new String(message.getBody()), exchange, replyText, routingKey);
    }
}

8.1.3 生产者

增加以下代码

@PostConstruct
public void init() {
     rabbitTemplate.setConfirmCallback(confirmCallback);
     // 回退消息回调
     rabbitTemplate.setReturnsCallback(confirmCallback);
 }

8.1.4 再次发送测试

在这里插入图片描述

8.3 备份交换机

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

8.3.1 代码结构图

在这里插入图片描述

8.3.2 修改配置类

@Configuration
public class ConfirmConfig {

    public static final String EXCHANGE_CONFIRM = "confirm.exchange";
    public static final String QUEUE_CONFIRM = "confirm.queue";
    public static final String ROUTING_KEY_CONFIRM = "confirm.routing.key";

    public static final String EXCHANGE_BACKUP = "backup.exchange";
    public static final String QUEUE_BACKUP = "backup.queue";
    public static final String QUEUE_WARNING = "warning.queue";

    @Bean
    public DirectExchange exchangeConfirm() {
        return ExchangeBuilder
                .directExchange(EXCHANGE_CONFIRM)
                .durable(true)
                // 设置该交换机的备份交换机
                .withArgument("alternate-exchange", EXCHANGE_BACKUP)
                .build();
    }

    @Bean
    public Queue queueConfirm() {
        return QueueBuilder.durable(QUEUE_CONFIRM).build();
    }

    @Bean
    public Binding bingingConfirmQueueToConfirmExchange(DirectExchange exchangeConfirm, Queue queueConfirm) {
        return BindingBuilder.bind(queueConfirm).to(exchangeConfirm).with(ROUTING_KEY_CONFIRM);
    }

    /**
     * 备份交换机
     */
    @Bean
    public FanoutExchange exchangeBackup() {
        return new FanoutExchange(EXCHANGE_BACKUP);
    }

    @Bean
    public Queue queueBackup() {
        return QueueBuilder.durable(QUEUE_BACKUP).build();
    }

    @Bean
    public Binding bingingBackupQueueToBackupExchange(FanoutExchange exchangeBackup, Queue queueBackup) {
        return BindingBuilder.bind(queueBackup).to(exchangeBackup);
    }

    /**
     * 报警队列
     */
    @Bean
    public Queue queueWarning() {
        return QueueBuilder.durable(QUEUE_WARNING).build();
    }

    @Bean
    public Binding bingingWarningQueueToBackupExchange(FanoutExchange exchangeBackup, Queue queueWarning) {
        return BindingBuilder.bind(queueWarning).to(exchangeBackup);
    }
}

8.3.3 报警消费者

@Slf4j
@Component
public class WarningConsumer {

    @RabbitListener(queues = ConfirmConfig.QUEUE_WARNING)
    public void receiver(Message message) {
        log.info("报警发现不可路由消息:{}", new String(message.getBody()));
    }
}

8.3.4 测试

在这里插入图片描述
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,备份交换机优先级高。

9 RabbitMQ 其它知识点

9.1 幂等性

用户对于同一操作发起的一次请求或者多次请求的结果是一致的。

9.1.1 消息重复消费

消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

9.1.2 解决思路

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。

9.1.3 消费端的幂等性保障

在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。

实现操作:
(1)唯一 ID+指纹码机制,利用数据库主键去重。
(2)利用 redis 的原子性去实现。

9.2 优先级队列

RabbitMQ 可以通过设置队列的优先级让后来的消息先得到消费。

9.2.1 优先级设置

页面上设置
在这里插入图片描述
代码中设置
在这里插入图片描述
设置消息的优先级
在这里插入图片描述

9.2.2 实例

消息生产者:

public class PriorityProducer {

    public static final String QUEUE_PRIORITY = "priority.queue";

    public static void main(String[] args) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            Map<String, Object> arguments = new HashMap<>();
            // 设置最大优先级
            arguments.put("x-max-priority", 10);
            channel.queueDeclare(QUEUE_PRIORITY, false, false, false, arguments);

            //该信息是用作演示队列个数限制
            for (int i = 1; i < 11; i++) {
                String message = "info" + i;

                if (i == 5) {
                    // 设置消息的 优先级 时间
                    AMQP.BasicProperties properties =
                            new AMQP.BasicProperties().builder().priority(5).build();
                    channel.basicPublish("", QUEUE_PRIORITY, properties, message.getBytes());
                } else {
                    channel.basicPublish("", QUEUE_PRIORITY, null, message.getBytes());
                }
                System.out.println("生产者发送消息:" + message);
            }
        }
    }
}

消息消费者:

public class PriorityConsumer {

    public static final String QUEUE_PRIORITY = "priority.queue";

    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();

        System.out.println("PriorityConsumer 等待接收消息...");

        // 接收消息
        channel.basicConsume(
                QUEUE_PRIORITY, true,
                (consumerTag, message) ->
                        System.out.println("PriorityConsumers 收到消息:message=" + new String(message.getBody())),
                consumerTag ->
                        System.out.println("消息消费被中断:" + consumerTag)
        );
    }
}

9.2.3 测试

先启动生产者:
在这里插入图片描述

再启动消费者:
在这里插入图片描述
高优先级的 info5 先被消费。

9.3 惰性队列

9.3.1 概念

RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。

9.3.2 两种模式

队列具备两种模式:default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。
在这里插入图片描述

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);

10 RabbitMQ 集群

10.1 集群搭建

(1)结构图
在这里插入图片描述
(2)准备三台虚拟机
hadoop102 hadoop103 hadoop104。每台机器上先先安装 RabbitMQ。

(3)确保各个节点的 cookie 文件使用的是同一个值

xsync /var/lib/rabbitmq/.erlang.cookie

(4)启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以下命令)

rabbitmq-server -detached

(5)再 hadoop103 执行

# (rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@hadoop102
# (只启动应用服务)
rabbitmqctl start_app

(6)再 hadoop104 执行

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@hadoop102
rabbitmqctl start_app

(7)集群状态

rabbitmqctl cluster_status

(8)重新设置用户
创建账号rabbitmqctl add_user admin 123```

设置用户角色 rabbitmqctl set_user_tags admin administrator

设置用户权限 rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

(9)如何解除集群节点(hadoop103 和 hadoop104 机器分别执行) 操作

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
# (hadoop102 机器上执行)
rabbitmqctl forget_cluster_node rabbit@hadoop103
rabbitmqctl forget_cluster_node rabbit@hadoop104

(10)集群节点
在这里插入图片描述

10.2 镜像队列

10.2.1 作用

引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。

10.2.3 使用

(1)启动集群
(2)随便找一个节点添加 policy
在这里插入图片描述

(3)再 hadoop102 上创建一个队列并发送消息
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值