RabbitMQ

RabbitMQ



目录

1. 简介
2. RabbitMQ安装
    2.1 rpm 安装
    2.2 docker 安装
3. Hello World
4. 工作队列
    4.1 轮询分发
    4.2 消息应答机制
    4.3 自动应答
    4.4 消息应答方式
    4.5 手动应答
    4.6 Multiple
    4.7 消息重新入队
    4.8 RabbitMQ持久化
    4.9 不公平分发
5. 发布确认
    5.1 开启确认发布
    5.2 单个确认发布
    5.3 批量确认发布
    5.4 异步确认发布
    5.5 三种确认发布的耗时对比
6. 交换机和绑定
    6.1 交换机
    6.2 绑定
7. 发布\订阅(Fanout)
8. Direct交换机
9. Topic交换机
10. 死信队列
    10.1 死信来源
    10.2 原理图
    10.3 消息TTL过期
    10.4 队列到达最大长度
11. 延迟队列(SpringBoot项目测试)
    11.1 RabbitMQ中的TTL
    11.2 队列TTL
    11.3 安装延迟队列插件
    11.4 docker 安装延迟队列插件
    11.5 使用延迟插件实现延迟队列
12. 集群
13. 镜像队列
14. Keepalived实现双机(主备)热备
15. 联邦交换机
16. 联邦队列
17. Shovel







回到目录

1. 简介


工作原理

rabbitmq_composition

名词介绍

  • Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker;
  • Virtual host:出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建 exchange/queue 等;
  • Connection:Publisher/Consumer和Broker之间的TCP连接;
  • Channel:如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个Thread创建单独的channel进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销
  • Exchange:message到达Broker的第一站,根据分发规则,匹配查询表中的 routingKey,分发消息到Queue 中去。常用的类型有:Direct(point-to-point), Topic(publish-subscribe) and fanout (multicast);
  • Queue:消息最终被送到这里等待 Consumer 取走;
  • Binding:Exchange 和 Queue 之间的虚拟连接,Binding 中可以包含 routingKey,Binding 信息被保 存到 Exchange 中的查询表中,用于 message 的分发依据。




回到目录

2. RabbitMQ 安装


(1)rpm安装
  1. 获取rpm包

Erlang的rpm包下载    Erlang的Github下载地址

RabbitMQ的rpm包下载

  1. 安装

注意:

CentOS 7 不能使用Erlang 24.x点击查看详细信息

echanges

# centos 7安装Erlang23.x
rpm -ivh erlang-23.3.4.11-1.el7.x86_64.rpm

# 检验Erlang是否安装成功(查看Erlang版本号)
elr -v
# Ctrl + c退出命令行,再输入a, enter退出

# 安装socat插件
yum install -y socat

# 安装RabbitMQ
rpm -ivh rabbitmq-server-3.9.13-1.el8.noarch.rpm
  1. 启动服务
# 启动服务
systemctl start rabbitmq-server

# 查看服务状态
systemctl status rabbitmq-server

# 停止服务
systemctl stop rabbitmq-server

# 添加开机启动 RabbitMQ 服务
systemctl enable rabbitmq-server

# 关闭开机启动 RabbitMQ 服务
systemctl disable rabbitmq-server
  1. 添加 web 管理插件
# 添加 web 管理插件
rabbitmq-plugins enable rabbitmq_management
# 默认只有localhost才能使用guest/guest访问

# 创建用户
rabbitmqctl add_user user password

# 设置用户分配操作权限
rabbitmqctl set_user_tags user administrator

# 为用户添加权限资源 vhost username permissions
rabbitmqctl set_permissions -p "/" user ".*" ".*" ".*"

# 修改密码
rabbitmqctl change_password username new_password

# 删除用户
rabbitmqctl delete_user username

# 查看用户清单
rabbitmqctl list_users

用户级别:

  • administrator:可以登录控制台、查看所有信息、可对rabbitmq进行管理
  • monitoring:登录控制台、查看所有信息
  • policymaker:登录控制台、指定策略
  • management:登录控制台
  1. 开放端口



回到目录

(2)docker安装
# 拉取镜像
docker pull rabbitmq:3.9-management

# 开启5672和15672端口
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent

# 添加或者移除端口, 需要重新加载才能生效
firewall-cmd --reload
# 云服务添加端口规则

# 运行RabbitMQ
# 各端口信息查看(https://www.rabbitmq.com/install-rpm.html)
# 4369:epmd,RabbitMQ 节点和 CLI 工具使用的对等发现服务
# 5671、5672:由不带和带 TLS 的 AMQP 0-9-1 和 1.0 客户端使用
# 15672:web管理插件
#################  不使用配置文件  #################
################# 对镜像端口全映射 #################
docker run -d --name rabbit \
-p 4369:4369 \
-p 5671:5671 \
-p 15671:15671 \
-p 5672:5672 \
-p 15672:15672 \
-p 15691:15691 \
-p 15692:15692 \
-p 25672:25672 \
-e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password \
--restart=always \
rabbitmq:3.9-management
################# 对镜像端口全映射 #################

################ 部分需要端口的映射 ################
docker run -d --name rabbit \
-p 5672:5672 \
-p 15672:15672 \
-e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password \
--restart=always \
rabbitmq:3.9-management
################## 需要部分的映射 ##################


################### 使用配置文件 #################
# 编写rabbitmq配置文件
touch /etc/rabbitmq/rabbitmq.conf
vim /etc/rabbitmq/rabbitmq.conf
# 添加默认用户名和密码(注意要在等号两边添加空格!!!!)
default_user = user
default_pass = password

# 运行
docker run -d --name rabbit \
-p 5672:5672 \
-p 15672:15672 \
-v /etc/rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \
-e RABBITMQ_CONFIG_FILE=/etc/rabbitmq/rabbitmq.conf \
--restart=always \
rabbitmq:3.9-management

RabbitMQ详细配置







回到目录

3. Hello World

建立maven工程对RabbitMQ进行简单使用。

hello_world


pom.xml

<dependencies>
    <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.10.0</version>
    </dependency>

    <!-- 操作文件流(后续用到) -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
    <!-- 消除警告
            SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
            SLF4J: Defaulting to no-operation (NOP) logger implementation
        -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
            <scope>compile</scope>
        </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>8</source>
                <target>8</target>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

Producer

public class Producer {
    public static final String QUEUE_NAME = "queue";

    public static void main(String[] args) {
        // 创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.37.100.249");
        factory.setUsername("user");
        factory.setPassword("password");
        // 自动关闭资源
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel()) {
            // 声明队列
            // 1. queue:        队列名称
            // 2. durable:      队列是否可以持久化,如果持久化,rabbitmq重启后队列还在
            // 3. exclusive:    是否独占连接,队列只允许再该连接中访问
            // 4. autoDelete:   自动删除,队列不再使用时是否自动删除此队列
            // 5. arguments:    参数,可以设置一个队列的扩展参数,比如:可设置存活时间
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            String msg = "hello, world!";
            // 1. exchange:     交换机,发到哪个交换机上。如果不指定将使用mq默认交换机(设置为"")
            // 2. routingKey:   路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
            // 3. props:        消息的属性
            // 4. body:         消息的内容
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
            System.out.println("消息发送完毕!");
        } catch (TimeoutException | IOException e) {
            e.printStackTrace();
        }
    }
}

运行Producer后
hello_world_producer

Consumer

public class Consumer {
    public static final String QUEUE_NAME = "queue";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.37.100.249");
        factory.setUsername("user");
        factory.setPassword("password");
        // 不释放资源,持续监听
        try {
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            System.out.println("等待接收信息....");
            // 推送的消息如何进行消费的接口回调
            // 1. consumerTag:  消费者标签,用来标识消费者的
            // 2. delivery:     封装消息的简单bean:public Delivery(Envelope envelope, AMQP.BasicProperties properties, byte[] body)
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println(msg);
            };
            // 取消消费的一个回调接口,如在消费的时候队列被删除掉了
            CancelCallback cancelCallback = (consumerTag) -> System.out.println("消息消费中断....");
            // 1. queue:             被消费的队列名称
            // 2. autoAck:           消费成功后,是否要自动应答
            // 3. deliverCallback:   消费成功后的回调
            // 4. cancelCallback:   消费失败后的回调
            channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

运行Consumer后

hello_world_consumer

注意点:

队列和交换机可以重复声明,但一定要先声明再使用,否则会报错;

重复声明时,要保持声明的一致性,否则会报错。







回到目录

4. Work Queues

工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们将任务安排在稍后完成。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将获取任务并最终执行作业。当有多个工作线程时,任务将在它们之间分发。

(1)轮询分发

在这里插入图片描述

抽取连接工具类:RabbitMqUtils

public class RabbitMqUtils {
    public static Channel getChannel() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.37.100.249");
        factory.setPort(5672);
        factory.setUsername("user");
        factory.setPassword("password");
        Connection connection = factory.newConnection();
        return connection.createChannel();
    }
}

启动两个工作线程:Consumer1和Consumer2

public class Consumer1 {
    public static final String QUEUE_NAME = "queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // AMQChannel(amqp://user@121.37.199.247:5672/,1)
        Channel channel = RabbitMqUtils.getChannel();
        // 声明队列
        // 1. queue:        队列名称
        // 2. durable:      队列是否可以持久化,如果持久化,rabbitmq重启后队列还在
        // 3. exclusive:    是否独占连接,队列只允许再该连接中访问
        // 4. autoDelete:   自动删除,队列不再使用时是否自动删除此队列
        // 5. arguments:    参数,可以设置一个队列的扩展参数,比如:可设置存活时间
        channel.queueDeclare(QUEUE_NAME, false, false, true, null);
        System.out.println("consumer1 等待接收信息.......");
        DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
            String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(consumerTag + ": consumer1消费信息:" + msg);
            try {
                // 模拟任务耗时1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        CancelCallback cancelCallback = (consumerTag -> System.out.println("consumer1 消息消费中断...."));
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}
public class Consumer2 {
    public static final String QUEUE_NAME = "queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // AMQChannel(amqp://user@121.37.199.247:5672/,1)
        Channel channel = RabbitMqUtils.getChannel();
  		// 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, true, null);
        System.out.println("consumer2 等待接收信息.......");
        DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
            String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(consumerTag + ": consumer2消费信息:" + msg);
        });
        CancelCallback cancelCallback = (consumerTag -> System.out.println(consumerTag + ": consumer2 消息消费中断...."));
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

在这里插入图片描述

启动一个发送线程

public class Producer {
    public static final String QUEUE_NAME = "queue";
    public static final int MESSAGE_NUM = 10;

    public static void main(String[] args) {
        try {
            // AMQChannel(amqp://user@121.37.199.247:5672/,1)
            Channel channel = RabbitMqUtils.getChannel();
            // 声明队列
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            for (int i = 1; i <= MESSAGE_NUM; i++)  {
                String msg = i + "";
                channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
                System.out.println("消息发送完毕: " + msg);
                Thread.sleep(i * 2);
            }
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述





(2)消息应答机制

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况?

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

为了保证消息在发送过程中不丢失,RabbitMQ引入消息应答机制。

消息应答就是:消费者在接收到消息并且处理该消息之后,告诉RabbitMQ它已经处理了,RabbitMQ可以把该消息删除了。


(3)自动应答

消息发送后立即被认为已经传送成功。

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

(4)消息应答的方式
  • Channel.basicAck: 用于肯定确认。RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了;
  • Channel.basicNack: 用于否定确认。
  • Channel.basicReject:用于否定确认。与Channel.basicNack相比少一个参数,不处理该消息了直接拒绝,可以将其丢弃了。
(5)手动应答

默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答。

只需对消费者的代码修改两处:

public class Consumer {
    public static final String QUEUE_NAME = "queue";

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("120.37.100.249");
        factory.setUsername("user");
        factory.setPassword("password");
        // 不释放资源,持续监听
        try {
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            System.out.println("等待接收信息....");
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println(msg);
                // 手动确认
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            CancelCallback cancelCallback = (consumerTag) -> System.out.println("消息消费中断....");
            // 手动确认需要把autoAck改为false
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}
(6)Multiple

手动应答的好处是可以批量应答并且减少网络拥堵。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sylowr0o-1648820123330)(assets\demo_02_Ack_Multiple.png)]

true和false之间的区别:
在这里插入图片描述


(7)消息重新入队

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

用Work Queue的代码实验:对于耗时较长的Consumer1工作进程,在其还未消费完队列里的消息时,将其停掉。

可以看出,Consumer在处理消息5时就断开了连接,RabbitMQ将了解到消息5以及后续消息未完全处理,并将对其重新排队,RabbitMQ将这些消息重新分发给Consumer2。


(8)RabbitMQ持久化

以上我们已经看到了如何处理任务不丢失的情况,但是如何保障当RabbitMQ服务停掉以后消息生产者发送过来的消息不丢失?

默认情况下RabbitMQ退出或由于某种原因崩溃时,它将忽视队列和消息,除非告知它不要这样做。

确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。

队列持久化

如果要队列实现持久化需要在声明队列的时候把 durable 参数设置为持久化(true)。
queue_durable

消息持久化

要想让消息实现持久化需要在消息生产者修改代码中basicPublish中的参数, 添加 MessageProperties.PERSISTENT_TEXT_PLAIN 这个属性。
durable


(9)不公平分发

有两个消费者在处理任务,其中Consumer1处理任务的速度非常慢,而另外一个Consumer2处理速度却很快,这个时候我们还是采用轮询分发的话,处理速度快的消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太合理,但是RabbitMQ并不知道这种情况,它依然很公平的进行分发。

为了避免这种情况,我们可以设置参数 channel.basicQos(int prefetchCount)

当prefetchCount=1时,这告诉RabbitMQ一次只向消费者发送一条消息, 在返回确认回执前, 不要向消费者发送新消息,而是把消息发给下一个空闲的消费者。





回到目录

5. 发布确认

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,Broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了。

如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,Broker回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 Broker 也可以设置 basicAck 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。

好处

confirm 模式最大的好处在于它是异步的。一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息。

如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。


(1)开启发布确认

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

// 开启发布确认
channel.confirmSelect();

(2)单个确认发布

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

这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。

public class Producer {
    public static final String QUEUE_NAME = "queue";
    public static final int MESSAGE_NUM = 1000;

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            // 开启发布确认
            channel.confirmSelect();
            System.out.println("-----任意输入后回车开始-----");
            new Scanner(System.in).next();
            long begin = System.currentTimeMillis();
            for (int i = 1; i <= MESSAGE_NUM; i++)  {
                String msg = i + "";
                channel.basicPublish("",QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
                // 服务端返回 false 或超时时间内未返回,生产者可以消息重发
                boolean flag = channel.waitForConfirms();
                if(flag) {
                    System.out.println("消息发送成功");
                }
            }
            long end = System.currentTimeMillis();
            System.out.println("发布" + MESSAGE_NUM + "个单独确认消息,耗时" + (end - begin) + "ms");
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}


(3)批量确认发布

与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量。

当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。

当然这种方案仍然是同步的,也一样阻塞消息的发布。

public class Producer2 {
    public static final String QUEUE_NAME = "queue";
    public static final int MESSAGE_NUM = 1000;

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            // 开启发布确认
            channel.confirmSelect();
            // 批量确认消息大小
            int batchSize = 100;
            // 未确定消息个数
            int outstandingMessageCount = 0;
            System.out.println("-----任意输入后回车开始-----");
            new Scanner(System.in).next();
            long begin = System.currentTimeMillis();
            for (int i = 1; i <= MESSAGE_NUM; i++)  {
                String msg = i + "";
                channel.basicPublish("",QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
                outstandingMessageCount++;
                if (outstandingMessageCount == batchSize) {
                    channel.waitForConfirms();
                    System.out.println("confirm......");
                    outstandingMessageCount = 0;
                }
            }
            // 为了确保没有剩余的确认消息,再次确认
            if (outstandingMessageCount > 0) {
                System.out.println("reconfirm..........");
                channel.waitForConfirms();
            }
            long end = System.currentTimeMillis();
            System.out.println("发布" + MESSAGE_NUM + "个单独确认消息,耗时" + (end - begin) + "ms");
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(4)异步发布确认

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高。它是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。

具体实现:Channel对象提供的ConfirmListener()回调方法只包含deliveryTag(当前Chanel发出的消息序号),我们需要自己为每一个Channel维护一个未确认消息序号的集合,每发布一条数据,集合中元素加1,
每回调一次handleAck方法,未确认消息序号的集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。

在这里插入图片描述

public class Producer3 {
    public static final int MESSAGE_NUM = 1000;

    public static void main(String[] args) {
        // 异步确认发布
        try (Channel channel = RabbitMqUtils.getChannel()){
            String queueName = UUID.randomUUID().toString();
            channel.queueDeclare(queueName, false, false, true, null);
            // 开启发布确认
            channel.confirmSelect();
            // ConcurrentSkipListMap线程安全有序,适用于高并发情况
            // 1. 可以做到轻松地将序号和消息进行关联
            // 2. 轻松批量删除条目,只要给到序列号
            // 3. 支持并发访问
            ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();

            // 确认收到信息的一个回调
            // 1. deliveryTag: 序列号(long型)
            // 2. multiple: ①true可以确认小于等于当前序列号的消息;②false确认当前序列号消息
            ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
                if (multiple) {
                    // 返回的是小于等于当前序列号的未确定消息
                    ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag, true);
                    // 清除该部分未确定的消息
                    confirmed.clear();
                } else {
                    // 只清除当前序列号消息
                    outstandingConfirms.remove(deliveryTag);
                }
            };

            ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
                String msg = outstandingConfirms.get(deliveryTag);
                System.out.println("发布的信息" + msg + "未被确认, 序列号" + deliveryTag);
            };

            // 添加一个异步确认的消息监听器
            channel.addConfirmListener(ackCallback, nackCallback);

            long begin = System.currentTimeMillis();
            for (int i = 1; i <= MESSAGE_NUM; i++) {
                String msg = i + "";
                // channel.getNextPublishSeqNo()获取下一个消息的版本号,从1开始
                // 通过序列号与消息体进行一个关联
                // 全部都是未确认的消息体
                outstandingConfirms.put(channel.getNextPublishSeqNo(), msg);
                channel.basicPublish("", queueName, null, msg.getBytes(StandardCharsets.UTF_8));
            }
            long end = System.currentTimeMillis();
            System.out.println("发布" + MESSAGE_NUM + "异步确认消息,耗时" + (end - begin) + "ms。");
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

(5)三种确认发布的耗时对比

ack_diff






回到目录

6. Exchanges、Bindings

(1)Exchanges

RabbitMQ 消息传递模型的核心思想是: 生产者从不会将生产的消息直接发送到队列。生产者只能将消息发送到交换机。
在这里插入图片描述

交换机工作的内容非常简单:一方面它接收来自生产者的消息,另一方面将它们推入队列。

交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这由交换机的类型来决定。



交换机的类型

  1. 直接(direct):定向,把消息交给符合指定routing key 的队列
  2. 主题(topic):通配符,把消息交给符合routing pattern(路由模式)的队列
  3. 标题(headers):
  4. 扇出(fanout):广播,将消息交给所有绑定到交换机的队列

无名exchange

默认交换机,使用空字符串(“”)进行标识。

// (String exchange, String routingKey, AMQP.BasicProperties props, byte[] body)
channel.basicPublish("",QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));

消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话。


(2)Bindings

Binding其实是 Exchange 和 Queue 之间的桥梁,它告诉我们 Exchange 和哪个队列进行了绑定关系。

channel.queueBind(String queueName, String exchangeName, String routingKey);




回到目录

7. 发布/订阅(Fanout)

Exchange将接收到的所有消息广播到它知道的所有队列中。

构建一个简单的日志记录系统,将所有消息广播给所有消费者:

EmitLog发送消息,Receive01将接收的消息打印在控制台,Receive02将接收的消息写入磁盘。

发布端:EmitLog

public class EmitLog {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) {
        try(Channel channel = RabbitMqUtils.getChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout", false, true, null);
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入信息:");
            while (scanner.hasNext()) {
                String msg = scanner.nextLine();
                channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes(StandardCharsets.UTF_8));
                System.out.println("生产者发出消息:" + msg);
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

订阅者:Receive01

public class Receive01 {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            // 声明一个交换机
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, false, true, null);
            // 生成一个临时的队列 队列的名称是随机的。当消费者断开和该队列的连接时,队列自动删除
            String queue = channel.queueDeclare().getQueue();
            // 把该临时队列绑定给定的exchange其中 routingKey(也称之为binding key)为空字符串
            channel.queueBind(queue, EXCHANGE_NAME, "");
            System.out.println("等待接收信息,把接收的信息打印在屏幕上....");
            DeliverCallback deliverCallback = ((consumerTag, message) -> {
                String msg = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println("控制台打印接收到的消息:" + msg);
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            });
            CancelCallback cancelCallback = consumerTag -> System.out.println("消息接受取消....");
            channel.basicConsume(queue, false, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

订阅者:Receive02

public class Receive02 {
    public static final String EXCHANGE_NAME = "logs";
    public static final String LOG_PATH = System.getProperty("user.dir") + "\\demo_04_Exchanges\\log.txt";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout", false, true, null);
            String queue = channel.queueDeclare().getQueue();
            channel.queueBind(queue, EXCHANGE_NAME, "");
            System.out.println("等待接收信息,把接收的信息写入log.txt");
            DeliverCallback deliverCallback = ((consumerTag, message) -> {
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String date = df.format(new Date());
                String msg = new String(message.getBody(), StandardCharsets.UTF_8);
                msg = date + "    " + msg + "\n";
                File file = new File(LOG_PATH);
                if (!file.exists()) {
                    boolean newFile = file.createNewFile();
                    if (!newFile) {
                        System.out.println("log.txt创建失败!");
                    }
                }
                FileUtils.writeStringToFile(file, msg, StandardCharsets.UTF_8, true);
                System.out.println("数据写入成功!");
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            });
            CancelCallback cancelCallback = consumerTag -> System.out.println("消息消费中断....");
            channel.basicConsume(queue, false, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

交换机绑定情况
在这里插入图片描述

结果展示
在这里插入图片描述





回到目录

8. Direct

Fanout这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播。

在这里我们将使用 Direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的 routingKey队列中去。

对上述使用Fanout类型交换机的日志系统进行更改:

将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。
在这里插入图片描述

Receive01:将严重错误写入磁盘

public class Receive01 {
    public static final String EXCHANGE_NAME = "logs";
    public static final String QUEUE_NAME = "disk";
    public static final String LOG_PATH = System.getProperty("user.dir") + "\\demo_05_Direct\\log.txt";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, false, true, null);
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
            System.out.println("等待接收信息......");
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String date = df.format(new Date());
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                msg = date + "    接收绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + msg + "\n";
                File file = new File(LOG_PATH);
                if (!file.exists()) {
                    boolean newFile = file.createNewFile();
                    if (!newFile) {
                        System.out.println("log.txt创建失败!");
                    }
                }
                FileUtils.writeStringToFile(file, msg, StandardCharsets.UTF_8, true);
                System.out.println("错误日志已经被接收。");
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            CancelCallback cancelCallback = consumerTag -> System.out.println("错误日志接收失败....");
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

Receive02:将警告和信息输出到控制台

public class Receive02 {
    public static final String EXCHANGE_NAME = "logs";
    public static final String QUEUE_NAME = "console";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, false, true, null);
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "warning");
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println(" 接收绑定键 :" + delivery.getEnvelope().getRoutingKey() + ", 消息:" + msg);
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> System.out.println("取消.."));
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

LogsDirect

public class LogsDirect {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) {
        try(Channel channel = RabbitMqUtils.getChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, false, true, null);
            // 创建多个bindingKey
            Map<String, String> bindingKeyMap = new HashMap<>(16);
            bindingKeyMap.put("info", "普通信息");
            bindingKeyMap.put("warning", "警告信息");
            bindingKeyMap.put("error", "错误信息");
            bindingKeyMap.put("debug", "调试信息");
            for (String bindingKey : bindingKeyMap.keySet()) {
                String msg = bindingKeyMap.get(bindingKey);
                channel.basicPublish(EXCHANGE_NAME, bindingKey, null, msg.getBytes(StandardCharsets.UTF_8));
                System.out.println("发出信息:" + msg);
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

交换机绑定情况
在这里插入图片描述





回到目录

9. Topics

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

在这个规则列表中, *可以代替一个单词,#可以替代零个或多个单词。
在这里插入图片描述

上图是一个队列绑定关系图,当rotingKey为

①quick.orange.rabbit时, 被队列Q1、Q2接收到

②lazy.orange.elephant时, 被队列Q1、Q2接收到

③quick.orange.fox时, 被队列Q1接收到

④lazy.brown.fox时, 被队列Q2接收到

⑤lazy.pink.rabbit时, 虽然满足两个绑定,但只被队列Q2接收一次

⑥quick.brown.fox时, 不匹配任何绑定不会被任何队列接收到,会被丢弃

根据绑定关系图,使用Topic类型的交换机改进日志系统

Receive01

public class Receive01 {
    public static final String EXCHANGE_NAME = "logs";
    public static final String QUEUE_NAME = "Q1";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, true, null);
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.orange.*");
            DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println(" 接收队列:" + QUEUE_NAME + "   绑定键:" + delivery.getEnvelope().getRoutingKey() + ",   消息:" + msg);
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            });
            CancelCallback cancelCallback = consumerTag -> System.out.println("取消....");
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

Receive02

public class Receive02 {
    public static final String EXCHANGE_NAME = "logs";
    public static final String QUEUE_NAME = "Q2";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, true, null);
            channel.queueDeclare(QUEUE_NAME, false, false, true, null);
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*.rabbit");
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "lazy.#");
            DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println(" 接收队列:" + QUEUE_NAME + "    绑定键:" + delivery.getEnvelope().getRoutingKey() + ",    消息:" + msg);
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            });
            CancelCallback cancelCallback = consumerTag -> System.out.println("取消....");
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

TopicLogs

public class TopicLogs {
    public static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, false, true, null);
            Map<String, String> bindingKeyMap = new HashMap<>(16);
            bindingKeyMap.put("quick.orange.rabbit", "被队列Q1、Q2接收到");
            bindingKeyMap.put("lazy.orange.elephant", "被队列Q1、Q2接收到");
            bindingKeyMap.put("quick.orange.fox", "被队列Q1接收到");
            bindingKeyMap.put("lazy.brown.fox", "被队列Q2接收到");
            bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定,但只被队列Q2接收一次");
            bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定,不会被任何队列接收到,会被丢弃");
            bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定,会被丢弃");
            bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词,但匹配Q2");
            for (String bindingKey : bindingKeyMap.keySet()) {
                String message = bindingKeyMap.get(bindingKey);
                channel.basicPublish(EXCHANGE_NAME, bindingKey, null, message.getBytes(StandardCharsets.UTF_8));
                System.out.println("生产者发出消息:" + message);
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

交换机绑定情况
在这里插入图片描述





回到目录

10. 死信队列(args)

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

应用场景:

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

(1)死信的来源
  • 消息 TTL 过期
  • 队列达到最大长度(队列满了,无法再添加数据到mq中)
  • 消息被拒绝(basicReject 或 basicNack)并且 requeue=false.
(2)原理图

在这里插入图片描述

(3)消息TTL过期

实验步骤:

  1. 启动Consumer01,让其声明死信交换机、死信队列、普通交换机和普通队列;
  2. 停止Consumer01,去图形化界面查看是否声明成功;(启动之后关闭该消费者,模拟其接收不到消息)
  3. 启动Producer,并观察图像化界面中的Queue —> messages中的变化
  4. 10秒后,再观察
  5. 启动consumer02对死信队列进行消费

Consumer01

public class Consumer01 {
    /**
     * 普通交换机名称
     */
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final String NORMAL_QUEUE = "normal_queue";
    /**
     * 死信交换机名称
     */
    public static final String DEAD_EXCHANGE = "dead_exchange";
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT, false, false, null);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT, false, false, null);

            // 声明死信队列
            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "LiSi");
            // 正常队列绑定死信队列
            Map<String, Object> params = new HashMap<>(16);
            // 正常队列设置死信交换机 参数key是固定值
            params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
            // 正常队列设置死信 routing-key 参数 key 是固定值
            params.put("x-dead-letter-routing-key", "LiSi");
            channel.queueDeclare(NORMAL_QUEUE, false, false, false, params);
            channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "ZhangSan");

            System.out.println("等待接收信息......");
            DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println("Consumer01接收到信息:" + msg);
            });
            CancelCallback cancelCallback = consumerTag -> System.out.println("消息消费中断....");
            channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

Consumer02:消费死信队列中的消息

public class Consumer02 {
    public static final String DEAD_EXCHANGE = "dead_exchange";
    public static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) {
        try {
            Channel channel = RabbitMqUtils.getChannel();
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT, false, false, null);
            channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
            channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "LiSi");
            DeliverCallback deliverCallback = ((consumerTag, delivery) -> {
                String msg = new String(delivery.getBody(), StandardCharsets.UTF_8);
                System.out.println("Consumer02 接收死信队列的消息: " + msg);
            });
            CancelCallback cancelCallback = consumerTag -> System.out.println("Consumer02 消息消费中断....");
            channel.basicConsume(DEAD_QUEUE,true, deliverCallback, cancelCallback);
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

Producer

public class Producer {
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    public static final int QUEUE_SIZE = 10;

    public static void main(String[] args) {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT, false, false, null);
            // 设置消息的TTL
            AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
            for (int i = 1; i <= QUEUE_SIZE; i++) {
                String msg = "info" + i;
                channel.basicPublish(NORMAL_EXCHANGE, "ZhangSan", properties, msg.getBytes(StandardCharsets.UTF_8));
                System.out.println("生产者发送信息:" + msg);
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
    }
}

过程:

启动Consumer01,让其声明死信交换机、死信队列、普通交换机和普通队列;停止Consumer01,去图形化界面查看是否声明成功;
在这里插入图片描述

启动Producer,并观察图像化界面中的Queue —> messages中的变化
在这里插入图片描述

10秒后,再观察
在这里插入图片描述

启动Consumer02对死信队列进行消费
在这里插入图片描述


(4)队列达到最大长度

修改Producer:去掉TTL属性

修改Consumer01:参数添加params.put("x-max-length", 10);,表示正常队列最大长度为10。





回到目录

11. 延迟队列(SpringBoot)

延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上。

延时队列中的元素是希望在指定时间到了以后或之前取出和处理。简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

使用场景

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

如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为"死信"。

如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用

如果不设置TTL,表示消息永远不会过期;

如果将TTL设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。

有两种方式设置 TTL:

消息设置TTL

@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMsg/{msg}")
    public void sendMsg(@PathVariable String msg) {
        rabbitTemplate.convertAndSend("X", "XC", msg, correlationData -> {
            correlationData.getMessageProperties().setExpiration("10");
            return correlationData;
        });
    }
}

队列设置TTL

/**
 * 声明队列A,ttl为10s并绑定到对应的死信交换机
 */
@Bean
public Queue queueA() {
    Map<String, Object> args = new HashMap<>(16);
    args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
    args.put("x-dead-letter-routing-key", "YD");
    args.put("x-message-ttl", 10000);
    return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}

两种方式的区别:

如果设置了队列的TTL属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中);

而设置了消息的TTL属性,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;


(2)队列TTL

创建两个队列QA和QB,两者队列TTL分别设置为10s和40s,然后再创建一个交换机X和死信交换机Y,它们的类型都是Direct,创建一个死信队列QD,它们的绑定关系:
在这里插入图片描述
pom.xml

<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>
    
</dependencies>

application.yml

spring:
  application:
    name: DelayQueue

  rabbitmq:
    host: 120.37.100.249
    port: 5672
    username: user
    password: password
# 应用服务 WEB 访问端口
server:
  port: 8080

TtlQueueConfig:配置队列和交换机

@Configuration
public class TtlQueueConfig {
    /**
     * 普通交换机
     */
    public static final String X_EXCHANGE = "X";
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    /**
     * 死信交换机
     */
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    /**
     * 死信队列
     */
    public static final String DEAD_LETTER_QUEUE = "QD";

    /**
     * 声明普通交换机
     */
    @Bean
    public DirectExchange xExchange() {
        return new DirectExchange(X_EXCHANGE);
    }

    /**
     * 声明死信交换机
     */
    @Bean
    public DirectExchange yExchange() {
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    /**
     * 声明队列A,ttl为10s并绑定到对应的死信交换机
     */
    @Bean
    public Queue queueA() {
        Map<String, Object> args = new HashMap<>(16);
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        args.put("x-dead-letter-routing-key", "YD");
        args.put("x-message-ttl", 10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }

    /**
     * 声明队列A绑定X交换机
     */
    @Bean
    public Binding aQueueBindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    /**
     * 声明队列B,ttl为40s并绑定到对应的死信交换机
     */
    @Bean
    public Queue queueB() {
        Map<String, Object> args = new HashMap<>(16);
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        args.put("x-dead-letter-routing-key", "YD");
        args.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
    }

    /**
     * 声明队列B绑定X交换机
     */
    @Bean
    public Binding bQueueBindingX(@Qualifier("queueB") Queue queueB, @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    /**
     * 声明死信队列QD
     */
    @Bean
    public Queue queueD() {
        return new Queue(DEAD_LETTER_QUEUE);
    }

    /**
     * 声明死信队列QD绑定死信交换机yExchange
     */
    @Bean
    public Binding deadLetterBindingQd(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange yExchange) {
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

SendMsgController:消息生产者

@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMsg/{msg}")
    public void sendMsg(@PathVariable String msg) {
        log.info("当前时间:{},发送一条消息给两个队列:{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), msg);
        
        rabbitTemplate.convertAndSend("X", "XA", "消息来自ttl为10s的队列:" + msg);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自ttl为40的队列:" + msg);
    }
}

DeadLetterQueueConsumer:死信队列消费者

@Slf4j
@Component
public class DeadLetterQueueConsumer {
    @RabbitListener(queues = "QD")
    public void receiveD(Message message) {
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info("当前时间:{},收到死信队列信息:{}", new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()), msg);
    }
}

http://localhost:8080/ttl/sendMsg/hello

第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉。
在这里插入图片描述



(3)安装延迟队列插件
  1. 官网上获取插件

    github地址(.ez文件)

  2. .ez文件复制到 RabbitMQ 的安装目录下的 plugins 目录下,接着进行安装

    # 复制文件
    cp rabbitmq_delayed_message_exchange-3.9.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.9.13/plugins/
    
    # 进入RabbitMQ 的安装目录下的 plgins 目录
    cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.9.13/plugins/
    
    # 启动延时插件,使插件生效(注意:不能带版本号,否则会报错{:plugins_not_found, [:"rabbitmq_delayed_message_exchange-3.9.0"]})
    rabbitmq-plugins enable rabbitmq_delayed_message_exchange
    

    安装前
    在这里插入图片描述

    安装后
    在这里插入图片描述



(4)docker安装延迟队列插件
  1. 官网上获取插件

    github地址(.ez文件)

  2. .ez文件复制到 docker容器的 /plugins 目录下,接着进行安装

    # rabbit为RabbitMQ容器名
    docker cp rabbitmq_delayed_message_exchange-3.9.0.ez rabbit:/plugins
    
    # 进入RabbitMQ容器
    docker exec -it rabbit /bin/bash
    
    # 启动延时插件,使插件生效
    rabbitmq-plugins enable rabbitmq_delayed_message_exchange
    

在这里插入图片描述



(5)使用RabbitMQ延迟插件实现延迟队列

CustomExchange:自定义交换机。

type=x-delayed-message 时,就使用到了上述插件。这是一种新的交换类型,该类型消息支持延迟投递机制。消息传递后并不会立即投递到目标队列中,而是存储在mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。

DelayedQueueConfig

@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";

    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

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

    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") CustomExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

生产者

@Slf4j
@RestController
@RequestMapping("ttl")
public class SendMsgController {
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendDelayMsg/{message}/{delayTime}")
    public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
        rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, correlationData -> {
            correlationData.getMessageProperties().setDelay(delayTime);
            return correlationData;
        });
        log.info("当前时间:{}, 发送一条延迟{}毫秒的信息给队列delayed.queue:{}",
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
                delayTime, message);
    }
}

消费者

@Slf4j
@Component
public class DelayedQueueConsumer {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";

    @RabbitListener(queues = DELAYED_QUEUE_NAME)
    public void receiveDelayedQueue(Message message) {
        String msg = new String(message.getBody(), StandardCharsets.UTF_8);
        log.info("当前时间:{}, 收到延迟队列的消息:{}",
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()),
                msg);
    }
}

发送一条延迟10s的消息给队列:

http://localhost:8080/ttl/sendDelayMsg/hello/10000
在这里插入图片描述







回到目录

12. 集群





回到目录

13. 镜像队列





回到目录

14. Keepalived实现双机(主备)热备





回到目录

15. 联邦交换机





回到目录

16. 联邦队列





回到目录

17. Shovel





  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值