RabbitMQ学习

文章目录

    • 一、MQ相关概念
      • 1、MQ
      • 2、作用
      • 3、实现MQ的两种主流方式:
      • 4、MQ的分类
      • 5、MQ的选择
    • 二、RabbitMQ
      • 1、RabbitMQ的概念
      • 2、四大核心概念
      • 3、核心组成部分
      • 4、五种消息模型
      • 5、安装
    • 三、五种消息模型
      • 1、[简单模型]-HelloWorld
        • 1.1 依赖
        • 1.2 消息生产者
        • 1.3 消息消费者
      • 2、[工作队列消息模型]-Work queues
        • 2.1 简介
        • 2.2 抽取工具类
        • 2.3 Work模式—轮询模式
          • 2.3.1 消费者(工作线程)
          • 2.3 .2生产者
        • 2.4 Work模式—能者多劳(不公平分发)
        • 2.5 预取值
      • 3、消息应答
        • 3.1 概念
        • 3.2 手动应答
          • 方法
          • 批量应答
          • 消息自动重新入队
          • 测试
      • 4、RabbitMQ持久化
        • 4.1 队列持久化
        • 4.2 消息实现持久化
        • 4.3 持久化总结
    • 四、发布确认
      • 1、发布确认原理
      • 2、发布确认策略
        • 2.1 单个确认发布
        • 2.2 批量确认发布
        • 2.3 异步确认发布
      • 3、如何处理异步未确认消息
      • 4、3种发布确认速度对比
    • 五、交换机
      • 1、Exchanges
        • 1.1 Exchanges 概念
        • 1.2.Exchanges 的类型
        • 1.3 无 名 exchange
      • 2、 临时队列
      • 3、绑定(bindings)
      • 4、订阅模式—Fanout
        • 4.1 生产者
        • 4.2 消费者01
        • 4.3 消费者02
        • 4.4 测试:
      • 5、订阅模型-Direct
        • 5.1 生产者
        • 5.2 消费者1
        • 5.3 消费者2
        • 5.4 测试
      • 6、订阅模型-Topic
        • 6.1 生产者
        • 6.2 消费者1
        • 6.3 消费者2
    • 六、死信队列
      • 1、死信的概念
      • 2、死信的来源
      • 3、死信实战
        • 3.1 代码架构图
        • 3.2 消 息 TTL 过期
          • 消费者01
          • 生产者:
          • 消费者02:
        • 3.3 队列达到最大长度
        • 3.4 消息被拒
    • 七、延迟队列
      • 7.1. 延迟队列概念
      • 7.2. 延迟队列使用场景
      • 7.3. RabbitMQ 中的 TTL
        • 7.3.1. 消息设置TTL
      • 7.4RabbitMQ 整合SpringBoot
        • 1、创建项目
        • 2、导入依赖
        • 3、添加配置文件
        • 4、Swagger配置类
        • 5、启动类
      • 7.5 队列TTL
        • 1、代码架构图
        • 2、配置文件类代码
        • 3、生产者
        • 4、消费者
        • 5、启动测试
        • 6、出现的错误
      • 7.6 延迟队列优化
        • 7.6.1 代码架构图
        • 7.6.2 配置文件
        • 7.6.3 生产者
        • 7.6.4 启动测试
      • 7.7. Rabbitmq 插件实现延迟队列
        • 7.7.1 安装延时队列插件
        • 7.7.2 代码架构图
        • 7.7.3 配置类
        • 7.7.4 生产者
        • 7.7.5 消费者
        • 7.7.6 测试
      • 7.8. 总结
    • 八、 发布确认高级
      • 8.1. 发布确认 springboot 版本
        • 8.1.1 确认机制方案
        • 8.1.2 代码架构图
        • 8.1.3 配置类
        • 8.1.4 消息生产者
        • 8.1.5 消息消费者
        • 8.1.6 测试1
        • 8.1.7 回调接口
        • 8.1.8 修改配置文件
        • 8.1.9 测试2
      • 8.2. 消息无法接收回退消息
        • 8.2.1 修改回调接口
        • 8.2.2 消息生产者代码
        • 8.2.3 测试
      • 8.3. 备份交换机
        • 8.3.1 代码架构图
        • 8.3.2 修改配置类
        • 8.3.3 警告消费者
        • 8.3.4 测试
    • 九、 RabbitMQ 其他知识点
      • 9.1. 幂等性
        • 9.1.1 概念
        • 9.1 .2 消息重复消费
        • 9.1.3解决思路
        • 9.1.4 消费端的幂等性保障
        • 9.1.5 唯 一 ID+指纹码机制
        • 9.1.6 Redis 原子性
      • 9.2. 优先级队列
        • 9.2.1 使用场景
        • 9.2.2 如何添加
        • 9.2.3 实战
      • 9.3. 惰性队列
        • 9.3.1 使用场景
        • 9.3.2 两种模式
        • 9.3.3 内存开销对比
    • 十、 RabbitMQ 集群
      • 10.1. clustering
        • 10.1.1 使用集群的原因
        • 10.1.2 内容
        • 10.1.2 普通集群模式
          • (1)拉取rabbitmq镜像
          • (2)创建映射数据卷目录
          • (3)创建容器
          • (4)访问
          • (5)容器节点加入集群
      • 10.2. 镜像队列
        • 10.2.1 使用镜像的原因
        • 10.2.2 搭建步骤

资料:

狂神:https://www.kuangstudy.com/zl/rabbitmq

乐优项目:https://www.yuque.com/lisha-wosnh/or1xy5/rzk38o

erlang 23.2- 24.X RabbitMq:3.9.X

https://beta.erlang.org/patches/otp-23.2

RabbitMQ安装成功!

使用Docker安装

image-20211123093249387

一、MQ相关概念

1、MQ

MQ:消息队列(message queue),本质是一个队列,在队列中存放消息,是一种跨进程的通信机制,用于上下游传递消息

2、作用

  • 流量削峰
  • 应用解耦
  • 异步处理

3、实现MQ的两种主流方式:

  • AMQP

RabbitMQ学习_第1张图片

  • JMS

RabbitMQ学习_第2张图片

两者间的区别和联系:

  • JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式

  • JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。

  • JMS规定了两种消息模型;而AMQP的消息模型更加丰富

常见MQ产品

  • ActiveMQ:基于JMS

  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好

  • RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会

  • Kafka:分布式消息系统,高吞吐量

4、MQ的分类

  • ActiveMQ
    • 优点:单机吞吐量万级,时效性 ms 级,可用性高,基于主从架构实现高可用性,消息可靠性较
      低的概率丢失数据
    • 缺点:维护越来越少,高吞吐量场景较少使用。
  • Kafka
    • 优点:为大数据而生,百万级TPS,吞吐量高,时效性ms级,分布式,优秀的第三方Web管理界面Kafka-Manager,日志领域成熟
    • 缺点:Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长,使用短轮询方式。消息失败不支持重试;支持消息顺序,但是一台代理宕机后,就会产生消息乱序,社区更新较慢;
  • RocketMQ
    • 优点:阿里开源产品,用Java实现;单机吞吐量十万级,分布式架构,扩展性好;消息可以做到0丢失;支持 10 亿级别的消息堆积,不会因为堆积导致性能下降
    • 缺点:仅支持Java和C++,其中C++不成熟
  • RabbitMQ
    • 优点:由于 erlang 语言的高并发特性,性能较好;吞吐量到万级,MQ 功能比较完备,健壮、稳定、易用、跨平台、支持多种语言 如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持 AJAX 文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高。
    • 缺点:商业版需要收费,学习成本较高

5、MQ的选择

  • kafka:大量数据,大型公司,有日志收集功能
  • RocketMQ:金融互联网,双十一
  • RabbitMQ:数据量没那么大,中小型公司

二、RabbitMQ

1、RabbitMQ的概念

是一个消息中间件,负责接收存储并转发消息。

2、四大核心概念

  • 生产者:产生数据发送消息的程序

  • 交换机:一方面接收来自生产者的消息,另一方面将消息推送到队列中。根据交换机的类型,处理接收到的消息,是推送到特定队列还是推送到多个队列,亦或者丢到消息

  • 队列:RabbitMQ内部的数据结构,用于存储消息,受主机的内存和磁盘限制的约束,本质上是一个消息缓冲区。

  • 消费者:消费者大多时候是一个等待接收消息的程序。

注意:消费者、生产者、中间件很多时候并不在一个机器上,同一个应用程序既可以是生产者又可以是消费者。

3、核心组成部分

RabbitMQ学习_第3张图片

RabbitMQ学习_第4张图片

Server:又称Broker ,接收和分发消息的应用,接收客户端的连接,实现AMQP实体服务。 安装rabbitmq-server

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 的开销

Message :消息,服务与应用程序之间传送的数据,由Propertiesbody组成,Properties可是对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则就是消息体的内容。

package org.springframework.amqp.core;
public class Message implements Serializable {
    
    private final MessageProperties messageProperties;

	private final byte[] body;
    public byte[] getBody() {
		return this.body; //NOSONAR
	}
	public MessageProperties getMessageProperties() {
		return this.messageProperties;
	}
    。。。。。。。。。。。。。。。。

}
package org.springframework.amqp.core;
/**
 * Message Properties for an AMQP message.
 */
public class MessageProperties implements Serializable {
    public static final Integer DEFAULT_PRIORITY = 0;
    private Integer priority = DEFAULT_PRIORITY;
    public void setPriority(Integer priority) {
		this.priority = priority;
	}

	public Integer getPriority() {
		return this.priority;
	}
    。。。。。。。。。。。。。。。。。。。。。。
}

Virtual Host :虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机理由可以有若干个Exhange和Queueu,同一个虚拟主机里面不能有相同名字的Exchange

Exchange:交换机,message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发
消息到 queue 中去。常用的类型有:direct (point-to-point)点对点,topic (publish-subscribe)发布订阅and fanout(multicast)广播。(交换机不具备消息存储的能力)

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

Routing key:是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息。

Queue:队列:也成为Message Queue,消息队列,保存消息并将它们转发给消费者。消息最终被送到这里等待 consumer 取走

4、五种消息模型

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。

但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

RabbitMQ学习_第5张图片

5、安装

使用Docker安装

三、五种消息模型

1、[简单模型]-HelloWorld

RabbitMQ学习_第6张图片

P(producer/ publisher):生产者,一个发送消息的用户应用程序。

C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序

队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。

1.1 依赖

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <configuration>
                    <source>8source>
                    <target>8target>
                configuration>
            plugin>
        plugins>
    build>

    <dependencies>
        
        <dependency>
            <groupId>com.rabbitmqgroupId>
            <artifactId>amqp-clientartifactId>
            <version>5.10.0version>
        dependency>
        
        <dependency>
            <groupId>commons-iogroupId>
            <artifactId>commons-ioartifactId>
            <version>2.6version>
        dependency>
    dependencies>
1.2 消息生产者
package com.lisa.rabitmq.oneimport com.rabbitmq.client.Channelimport com.rabbitmq.client.Connectionimport com.rabbitmq.client.ConnectionFactoryimport java.io.IOExceptionimport java.util.concurrent.TimeoutException/**
 * 生产者
 */
public class Producer {
    private static final String QUEUE_NAME = "hello"public static void main(String[] args) throws IOExceptionTimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin")Connection connection = factory.newConnection()//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Channel channel = connection.createChannel()/**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)String message = "hello Lisa"/**
         * 发送一个消息
         * 1.发送到那个交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes())System.out.println("消息发送完毕!")}
}

启动后看到有一个消息Ready 状态

RabbitMQ学习_第7张图片

点击队列获取消息的内容:

RabbitMQ学习_第8张图片

1.3 消息消费者
package com.lisa.rabitmq.oneimport com.rabbitmq.client.*import java.io.IOExceptionimport java.util.concurrent.TimeoutException/**
 * 消费者
 */
public class Customer {
    private static final String QUEUE_NAME = "hello"public static void main(String[] args) throws IOExceptionTimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin")Connection connection = factory.newConnection()//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Channel channel = connection.createChannel()System.out.println("等待接收消息!")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            System.out.println(new String(message.getBody()).toString())}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")}/**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}

启动,接收到消息

RabbitMQ学习_第9张图片

已经显示消息被消费了

RabbitMQ学习_第10张图片

2、[工作队列消息模型]-Work queues

2.1 简介

工作队列或者竞争消费者模式

RabbitMQ学习_第11张图片

工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。
相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取

P:生产者:任务的发布者

C1:消费者,领取任务并且完成任务,假设完成速度较快

C2:消费者2:领取任务并完成任务,假设完成速度慢

面试题:避免消息堆积?

1)采用workqueue,多个消费者监听同一队列。

2)接收到消息以后,而是通过线程池,异步消费。

image-20211124143122905

当有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?

主要有两种模式:

1、轮询模式的分发:一个消费者一条,按均分配;

2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;

2.2 抽取工具类
package com.lisa.rabitmq.utilsimport com.rabbitmq.client.Channelimport com.rabbitmq.client.Connectionimport com.rabbitmq.client.ConnectionFactory/**
 * 抽取工具类:使用连接工厂创建连接的信道channel
 */
public class RabbitMQUtils {
    public static Channel getChannel() throws Exception{
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin")Connection connection = factory.newConnection()//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Channel channel = connection.createChannel()return channel;
    }
}
2.3 Work模式—轮询模式
2.3.1 消费者(工作线程)
package com.lisa.rabitmq.twoimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 第一个工作线程(消费者)
 */
public class Work01 {
    public static final String QUEUE_NAME = "hello"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            System.out.println("接收到的消息:"+new String(message.getBody()).toString())}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")}/**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        System.out.println("C2消费者启动消息等待。。。");
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}

启动两个工作线程,需要IDEA打开[Allow multiple instances],然后再次启动工作线程

RabbitMQ学习_第12张图片

启动后效果:

RabbitMQ学习_第13张图片

RabbitMQ学习_第14张图片

2.3 .2生产者
package com.lisa.rabitmq.twoimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.Channelimport java.util.Scanner/**
 * 生产者
 */
public class Task01 {
    public static final String QUEUE_NAME = "hello"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()/**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//从控制台中接收消息
        Scanner scanner = new Scanner(System.in)while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes())System.out.println("发送消息完成:"+message)}
    }
}

启动后,在控制台输入

RabbitMQ学习_第15张图片

同时C1和C2消费者接收到信息:

RabbitMQ学习_第16张图片

RabbitMQ学习_第17张图片

通过程序执行发现生产者总共发送 4 个消息,消费者 1 和消费者 2 分别分得两个消息,并且
是按照有序的一个接收一次消息

2.4 Work模式—能者多劳(不公平分发)

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但RabbitMQ 并不知道这种情况,它依然很公平的进行分发。

  • 特点:由于消息接收者处理消息的能力不同,存在处理快慢的问题,我们就需要能者多劳,处理快的多处理,处理慢的少处理;

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

在消费者消费消息之前设置不公平分发:

package com.lisa.rabitmq.three;
/**
 * 消费者1
 *
 */
public class Work02 {
    public static final String QUEUE_NAME = "ack_queue";

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

        //消息消费的时候如何处理消息  模拟处理速度:沉睡1秒
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            SleepUtils.sleep(1);
            System.out.println("接收到的消息:"+new String(message.getBody()).toString());
            /**
             * 消息应答:
             * 1、消息标记tag
             * 2、是否批量应答未应答的消息
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消费者取消消费接口回调逻辑!");
        };

        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        System.out.println("C1消费者启动消息等待。。。");
        //设置不公平分发
        int prefetchCount = 1;
        channel.basicQos(prefetchCount);
        //采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}
package com.lisa.rabitmq.three;
/**
 * 消费者2
 *
 */
public class Work03 {
    public static final String QUEUE_NAME = "ack_queue";

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

        //消息消费的时候如何处理消息  模拟处理速度:沉睡30秒
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            SleepUtils.sleep(30);
            System.out.println("接收到的消息:"+new String(message.getBody()).toString());
            /**
             * 消息应答:
             * 1、消息标记tag
             * 2、是否批量应答未应答的消息
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };

        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消费者取消消费接口回调逻辑!");
        };

        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        System.out.println("C2消费者启动消息等待。。。");
        //设置不公平分发
        int prefetchCount = 1;
        channel.basicQos(prefetchCount);
        //采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}

测试:

生产者连续发送5条消息:

RabbitMQ学习_第18张图片

消费者C1:

RabbitMQ学习_第19张图片

消费者C2:

RabbitMQ学习_第20张图片

实现了不公平分发!

RabbitMQ学习_第21张图片

2.5 预取值

RabbitMQ学习_第22张图片

设置预取值C1为2,C2为5

		//设置不公平分发
        //int prefetchCount = 1;

        //预取值
        int prefetchCount = 2;
        channel.basicQos(prefetchCount)//采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback)

RabbitMQ学习_第23张图片

3、消息应答

3.1 概念

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

因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被

接收。不过这种回执ACK分两种情况:

  • 自动ACK:消息一旦被接收,消费者自动发送ACK

  • 手动ACK:消息接收后,不会发送ACK,需要手动调用

大家觉得哪种更好呢?

这需要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便

  • 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。

之前的应答方式都是自动应答,现在来测试手动应答

3.2 手动应答
方法
  • Channel.basicAck(用于肯定确认)

    RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了

  • Channel.basicNack(用于否定确认)

  • Channel.basicReject(用于否定确认)

    与 Channel.basicNack 相比少一个参数

    不处理该消息了直接拒绝,可以将其丢弃了

批量应答

手动应答的好处是可以批量应答并且减少网络拥堵

RabbitMQ学习_第24张图片

multiple 的 true 和 false 代表不同意思

  • true :代表批量应答 channel 上未应答的消息

    比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答

  • false: 只应答当前的消息。同上面相比只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答

RabbitMQ学习_第25张图片

消息自动重新入队

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

RabbitMQ学习_第26张图片

图一:消费者C1处理消息1,消费者C2处理消息2

图二:当消费者C1在处理消息时,突然失去连接,这时消息还未被处理完成,没有确认

图三:RabbitMQ 将了解到消息未完全处理,并将对其重新排队

图四:MQ 将消息分发给消费者C2进行处理

测试

消费者

package com.lisa.rabitmq.threeimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.Channelimport java.util.Scanner/**
 * 生产者
 * 消息手动应答
 */
public class Task02 {
    public static final String QUEUE_NAME = "ack_queue"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel();
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//从控制台中接收消息
        Scanner scanner = new Scanner(System.in)while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"))System.out.println("发送消息完成:"+message)}
    }
}

消费者C1

package com.lisa.rabitmq.threeimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.lisa.rabitmq.utils.SleepUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 消费者
 *
 */
public class Work02 {
    public static final String QUEUE_NAME = "ack_queue"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()System.out.println("C1等待接收消息处理时间较短")//消息消费的时候如何处理消息
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            SleepUtils.sleep(1)System.out.println("接收到的消息:"+new String(message.getBody()).toString())/**
             * 消息应答:
             * 1、消息标记tag
             * 2、是否批量应答未应答的消息
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag()false)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消费者取消消费接口回调逻辑!")}/**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        System.out.println("C1消费者启动消息等待。。。")//采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback)}
}

消费者C2

package com.lisa.rabitmq.threeimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.lisa.rabitmq.utils.SleepUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 消费者
 *
 */
public class Work03 {
    public static final String QUEUE_NAME = "ack_queue"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()System.out.println("C2等待接收消息处理时间较长")//消息消费的时候如何处理消息
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            SleepUtils.sleep(30)System.out.println("接收到的消息:"+new String(message.getBody()).toString())/**
             * 消息应答:
             * 1、消息标记tag
             * 2、是否批量应答未应答的消息
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag()false)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消费者取消消费接口回调逻辑!")}/**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        System.out.println("C2消费者启动消息等待。。。")//采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback)}
}

工具类:

package com.lisa.rabitmq.utils/**
 * 睡眠工具类
 */
public class SleepUtils {
    public static void sleep(int second){
        try {
            Thread.sleep(1000 * second)} catch (InterruptedException _ignored) {
            Thread.currentThread().interrupt()}
    }
}

启动测试:

启动生产者,会看到队列

RabbitMQ学习_第27张图片

然后启动消费者C1和C2:

  • 生产者输入AA 、BB C1 收到AA C2收到BB
  • 生产者输入CC、DD C1 收到CC C2收到DD
  • 生产者输入EE、FF 停止C2 只用C1收到EE、FF

RabbitMQ学习_第28张图片

4、RabbitMQ持久化

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

4.1 队列持久化

之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化需要在声明队列的时候把 durable 参数设置为持久化

 		/**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
//让消息队列持久化
 boolean durable = true;
 channel.queueDeclare(QUEUE_NAME,durable,falsefalsenull)
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<StringObject> arguments) throws IOException

但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误

#method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'ack_queue' in vhost '/': received 'true' but current is 'false'class-id=50, method-id=10)

删除当前的队列ack_queue

image-20211125111446471

RabbitMQ学习_第29张图片

RabbitMQ学习_第30张图片

然后再启动生产者,会看到该队列持久化的特性:D(持久化)

以下为控制台中持久化与非持久化队列的 UI 显示区、

RabbitMQ学习_第31张图片

这个时候即使重启 rabbitmq 队列也依然存在

4.2 消息实现持久化

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

channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"))

将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,参考后边课件发布确认章节。

4.3 持久化总结

交换机持久化:

image-20211126164603250

队列持久化:

image-20211126164702866

消息持久化:

image-20211126164822543

四、发布确认

1、发布确认原理

image-20211125153545178

2、发布确认策略

RabbitMQ学习_第32张图片

2.1 单个确认发布
//1、单个确认发布   发布1000个单个确认消息,耗时178ms
    public static void messageConfirmSingle() throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//队列名称
        String queueName = UUID.randomUUID().toString()//声明队列
        channel.queueDeclare(queueName,truefalsefalsenull)//开启发布确认
        channel.confirmSelect()long begin = System.currentTimeMillis()//批量发消息
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + "";
            channel.basicPublish("",queueName,null,message.getBytes())//单个消息发送后就马上进行发布确认
            boolean flag = channel.waitForConfirms()if (flag){
                System.out.println("消息发布成功!")}
        }
         
        long end = System.currentTimeMillis()System.out.println("发布" + MESSAGE_COUNT + "个单个确认消息,耗时" + (end - begin) + "ms")}
2.2 批量确认发布
//2、批量确认发布
    public static void messageConfirmBatch() throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//队列名称
        String queueName = UUID.randomUUID().toString()//声明队列
        channel.queueDeclare(queueName,truefalsefalsenull)//开启发布确认
        channel.confirmSelect()long begin = System.currentTimeMillis()//批量确认消息大小
        int batchSize = 100//批量发消息  批量确认发布
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + ""//发送消息
            channel.basicPublish("",queueName,null,message.getBytes())//每100个消息就批量确认发布
            if(i % batchSize == 0){
                //发布确认
                channel.waitForConfirms()}
        }
        long end = System.currentTimeMillis()System.out.println("发布" + MESSAGE_COUNT + "个批量确认发布消息,耗时" + (end - begin) + "ms")}
2.3 异步确认发布

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

RabbitMQ学习_第33张图片

  • 发消息之前准备消息监听器,监听哪些消息成功了,哪些消息失败了

     //消息确认成功 回调函数
        ConfirmCallback ackCallback = (deliveryTag , multiple) -> {
            System.out.println("确认的消息的标记:" + deliveryTag)}//消息确认失败 回调函数
        ConfirmCallback nackCallback = (deliveryTag , multiple) -> {
            System.out.println("未确认的消息的标记:" + deliveryTag)}/**
         * 添加异步确认的监听器:监听哪些消息成功了  哪些消息失败了
         */
        channel.addConfirmListener(ackCallback,nackCallback)//异步通知
    
  • 然后发布消息

    channel.basicPublish("",queueName,null,message.getBytes())

总代码:

//3、异步批量确认发布
public static void messageConfirmAsync() throws Exception{
    Channel channel = RabbitMQUtils.getChannel()//队列名称
    String queueName = UUID.randomUUID().toString()//声明队列
    channel.queueDeclare(queueName,truefalsefalsenull)//开启发布确认
    channel.confirmSelect()//消息确认成功 回调函数
    ConfirmCallback ackCallback = (deliveryTag , multiple) -> {
        System.out.println("确认的消息的标记:" + deliveryTag)}//消息确认失败 回调函数
    ConfirmCallback nackCallback = (deliveryTag , multiple) -> {
        System.out.println("未确认的消息的标记:" + deliveryTag)}/**
     * 添加异步确认的监听器:监听哪些消息成功了  哪些消息失败了
     */
    channel.addConfirmListener(ackCallback,nackCallback)//异步通知

    long begin = System.currentTimeMillis()//批量发消息
    for (int i = 0; i < MESSAGE_COUNT; i++) {
        String message = i + "";
        channel.basicPublish("",queueName,null,message.getBytes())}
    long end = System.currentTimeMillis()System.out.println("发布" + MESSAGE_COUNT + "个异步确认发布消息,耗时" + (end - begin) + "ms")}

3、如何处理异步未确认消息

思路:

  • 发送消息前记录下要发送的消息 总和
  • 在消息确认成功的回调函数中 删除已经确认的消息
  • 剩下的就是未确认的消息

过程:

  • 创建一个Map,用于存储消息(key:消息序列号,value:消息)

    ConcurrentSkipListMap<ObjectObject> outstandingConfirms = new ConcurrentSkipListMap<>()
  • 批量发消息的时候,先将消息存储到这个Map中去(现在这些消息时未确认的),然后再发送

    //channel.getNextPublishSeqNo() 获取下一个消息的序列号 
    outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
    channel.basicPublish("",queueName,null,message.getBytes())
  • 这时候异步确认的监听器起作用了

    • 消息确认成功回调函数

      • 如果是批量确认,就将这些消息批量删除

      • 如果时单个确认,就删除当前序列号的消息

        			if(multiple){
                        //返回的是小于等于当前序列号的未确认消息 是一个 map
                        ConcurrentNavigableMap<ObjectObject> confirmed = outstandingConfirms.headMap(deliveryTag, true)//消除该部分未确认的消息
                        confirmed.clear()}else{
                        //只消除当前序列号的消息
                        outstandingConfirms.remove(deliveryTag)}
        
    • 消息确认失败回调函数

      • 就可以根据序列号查询未确认的消息,然后输出

         String message = (String) outstandingConfirms.get(deliveryTag)System.out.println("发布的消息"+message+"未被确认,序号"+deliveryTag)

总代码:

//3、异步批量确认发布
    public static void messageConfirmAsync() throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//队列名称
        String queueName = UUID.randomUUID().toString()//声明队列
        channel.queueDeclare(queueName,truefalsefalsenull)//开启发布确认
        channel.confirmSelect()/**
         * 线程安全有序的一个哈希表,适用于高并发的情况
         * 1.轻松的将序号与消息进行关联
         * 2.轻松批量删除条目 只要给到序列号
         * 3.支持并发访问
         */
        ConcurrentSkipListMap<ObjectObject> outstandingConfirms = new ConcurrentSkipListMap<>()//消息确认成功 回调函数
        /**
         * 确认收到消息的一个回调
         * 1.deliveryTag:消息序列号
         * 2.multiple:是否为批量确认
         *            true 批量确认,可以确认小于等于当前序列号的消息
         *            false 单个确认  确认当前序列号消息
         */

        ConfirmCallback ackCallback = (deliveryTag , multiple) -> {
            System.out.println("确认的消息的标记:" + deliveryTag)if(multiple){
                //返回的是小于等于当前序列号的未确认消息 是一个 map
                ConcurrentNavigableMap<ObjectObject> confirmed = outstandingConfirms.headMap(deliveryTag, true)//消除该部分未确认的消息
                confirmed.clear()}else{
                //只消除当前序列号的消息
                outstandingConfirms.remove(deliveryTag)}
        }//消息确认失败 回调函数
        ConfirmCallback nackCallback = (deliveryTag , multiple) -> {
            String message = (String) outstandingConfirms.get(deliveryTag)System.out.println("发布的消息"+message+"未被确认,序列号"+deliveryTag)}/**
         * 添加一个异步确认的监听器   监听哪些消息成功了  哪些消息失败了
         * 1.确认收到消息的回调
         * 2.未收到消息的回调
         *
         */
        channel.addConfirmListener(ackCallback,nackCallback)//异步通知

        long begin = System.currentTimeMillis()//批量发消息
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + ""/**
             * channel.getNextPublishSeqNo() 获取下一个消息的序列号
             * 通过序列号与消息体进行一个关联
             * 全部都是未确认的消息体
             * 放入map中去
             */
            outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
            channel.basicPublish("",queueName,null,message.getBytes())}
        long end = System.currentTimeMillis()System.out.println("发布" + MESSAGE_COUNT + "个异步确认发布消息,耗时" + (end - begin) + "ms")}

4、3种发布确认速度对比

  • 单独发布消息

    同步等待确认,简单,但吞吐量非常有限。

  • 批量发布消息

    批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。

  • 异步处理:

    最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些

五、交换机

RabbitMQ学习_第34张图片

1、Exchanges

1.1 Exchanges 概念

RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产

者甚至都不知道这些消息传递传递到了哪些队列中。相反,生产者只能将消息发送到交换机(exchange),

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

切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢

弃它们。这就的由交换机的类型来决定。

1.2.Exchanges 的类型

总共有以下类型:

直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)

RabbitMQ学习_第35张图片

1.3 无 名 exchange

在本教程的前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的
原因是因为我们使用的是默认交换机,我们通过空字符串(“”)进行标识。

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

第一个参数是交换机的名称。**空字符串表示默认或无名称交换机:**消息能路由发送到队列中其实是由routingKey(bindingkey)绑定 key 指定的,如果它存在的话。

2、 临时队列

之前的章节我们使用的是具有特定名称的队列(还记得 hello 和 ack_queue 吗?)。队列的名称我们来说至关重要,我们需要指定我们的消费者去消费哪个队列的消息。每当我们连接到 RabbitMQ 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。

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

String queueName = channel.queueDeclare().getQueue()

创建出来之后长成这样:

RabbitMQ学习_第36张图片

3、绑定(bindings)

什么是 bingding 呢,binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队

列进行了绑定关系。

RabbitMQ学习_第37张图片

RabbitMQ学习_第38张图片

4、订阅模式—Fanout

Fanout,也称为广播。

流程图:

RabbitMQ学习_第39张图片

在广播模式下,消息发送流程是这样的:

  • 1) 可以有多个消费者

  • 2) 每个消费者有自己的queue(队列)

  • 3) 每个队列都要绑定到Exchange(交换机)

  • 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。

  • 5) 交换机把消息发送给绑定过的所有队列

  • 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

4.1 生产者
package com.lisa.rabitmq.fiveimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.Channelimport java.util.Scanner/**
 * 发布订阅模式——fanout
 * 生产者
 */
public class Producer2 {
    public static final String EXCHANGE_NAME = "exchange_fanout"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明exchange,指定为fanout
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout")//channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        Scanner scanner = new Scanner(System.in)while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish(EXCHANGE_NAME,""null,message.getBytes())System.out.println("生产者发送消息完成:"+message)}
    }
}
4.2 消费者01
package com.lisa.rabitmq.fiveimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 发布订阅模式——fanout
 * 消费者1
 *
 */
public class Consumer01 {
    public static final String EXCHANGE_NAME = "exchange_fanout"public static final String QUEUE_NAME = "queue01_fanout"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明队列
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//绑定队列到交换机
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"")System.out.println("等待接收消息,把接收到的消息打印在屏幕........... ")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()).toString()System.out.println("消费者1 控制台打印接收到的消息:"+ m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}
4.3 消费者02
package com.lisa.rabitmq.fiveimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 发布订阅模式——fanout
 * 消费者1
 *
 */
public class Consumer02 {
    public static final String EXCHANGE_NAME = "exchange_fanout"public static final String QUEUE_NAME = "queue02_fanout"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明队列
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//绑定队列到交换机
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"")System.out.println("等待接收消息,把接收到的消息打印在屏幕........... ")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()).toString()System.out.println("消费者2 控制台打印接收到的消息:"+ m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}
4.4 测试:

先启动生产者,发送消息

RabbitMQ学习_第40张图片

消费者1收到消息:

RabbitMQ学习_第41张图片

消费者2收到消息:

RabbitMQ学习_第42张图片

BuiltinExchangeType:交换机的类型

package com.rabbitmq.client/**
 * Enum for built-in exchange types.
 */
public enum BuiltinExchangeType {

    DIRECT("direct")FANOUT("fanout")TOPIC("topic")HEADERS("headers")private final String type;

    BuiltinExchangeType(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

5、订阅模型-Direct

有选择性的接收消息

在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。

在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息

引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)

消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

RabbitMQ学习_第43张图片

  • 生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
  • Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
  • 消费者1,其所在队列指定了需要routing key 为 error 的消息
  • 消费者2,其所在队列指定了需要routing key 为 info、warning 的消息
  • 但是由于没有队列绑定debug消息,该消息会被丢弃
5.1 生产者
package com.lisa.rabitmq.directimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.Channelimport java.util.HashMapimport java.util.Mapimport java.util.Scanner/**
 * 发布订阅模式——direct
 * 生产者
 */
public class Producer3 {
    public static final String EXCHANGE_NAME = "exchange_direct"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明exchange,指定为direct
        channel.exchangeDeclare(EXCHANGE_NAME,"direct")//创建多个bindingkey
        Map<StringString> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("info""普通info消息");
        bindingKeyMap.put("warning""警告 warning 信息");
        bindingKeyMap.put("error""错误 error 信息")//debug 没有消费这接收这个消息 所有就丢失了
        bindingKeyMap.put("debug""调试 debug 信息")for (Map.Entry<StringString> bindingKeyEntry : bindingKeyMap.entrySet()) {
            //获取绑定键
            String bindingKey = bindingKeyEntry.getKey()//info  warning   error debug
            String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME,bindingKey,null,message.getBytes("UTF-8"))System.out.println("生产者发出的消息:" + message)}
    }
}
5.2 消费者1

routingkey:error

package com.lisa.rabitmq.directimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 发布订阅模式——direct
 * 消费者1
 *
 */
public class Consumer03 {
    public static final String EXCHANGE_NAME = "exchange_direct"public static final String QUEUE_NAME = "queue01_direct"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明队列
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//绑定队列到交换机  绑定到error
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"error")System.out.println("消费者1等待接收消息,把接收到的消息打印在屏幕........... ")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()"UTF-8").toString();

            m = "接收绑定键:" + message.getEnvelope().getRoutingKey()+"消息:"+m.toString()System.out.println("消费者1 控制台打印接收到的消息:"+ m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}
5.3 消费者2

routingkey:info warning

package com.lisa.rabitmq.directimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 发布订阅模式——direct
 * 消费者1
 *
 */
public class Consumer04 {
    public static final String EXCHANGE_NAME = "exchange_direct"public static final String QUEUE_NAME = "queue02_direct"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明队列
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//绑定队列到交换机  绑定到info  warning
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"info");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"warning")System.out.println("消费者2等待接收消息,把接收到的消息打印在屏幕........... ")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()"UTF-8").toString();

            m = "接收绑定键:" + message.getEnvelope().getRoutingKey()+"消息:"+m.toString()System.out.println("消费者2 控制台打印接收到的消息:"+ m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}
5.4 测试

生产者

RabbitMQ学习_第44张图片

消费者1

RabbitMQ学习_第45张图片

消费者2

RabbitMQ学习_第46张图片

6、订阅模型-Topic

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。

只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!

Routingkey一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如:item.insert

通配符规则:

  • #:匹配一个或多个词

  • *:匹配不多不少恰好1个词

audit.#:能够匹配audit.irs.corporate 或者 audit.irs

audit.*:只能匹配audit.irs

RabbitMQ学习_第47张图片

我们创建了三个绑定:

Q1绑定了绑定键*.orange.* Q1匹配所有的橙色动物。

Q2绑定了*.*.rabbitlazy.#。 Q2匹配关于兔子以及懒惰动物的消息。

练习:

quick.orange.rabbit 被队列 Q1Q2 接收到

lazy.orange.elephant 被队列 Q1Q2 接收到

quick.orange.fox Q1

lazy.brown.fox Q2

lazy.pink.rabbit Q2

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

quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃

lazy.orange.male.rabbit 是四个单词但匹配 Q2

6.1 生产者
package com.lisa.rabitmq.topicimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.Channelimport java.util.HashMapimport java.util.Mapimport java.util.Scanner/**
 * 发布订阅模式——topic
 * 生产者
 */
public class Producer4 {
    public static final String EXCHANGE_NAME = "Topic2"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明exchange,指定为topic
        channel.exchangeDeclare(EXCHANGE_NAME,"topic")//创建多个bindingkey
        Map<StringString> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("quick.orange.rabbit""1)被队列 Q1 Q2 接收到");
        bindingKeyMap.put("quick.orange.fox""2) 被队列 Q1 接收到");
        bindingKeyMap.put("lazy.brown.fox""3) 被队列 Q2 接收到")for (Map.Entry<StringString> bindingKeyEntry : bindingKeyMap.entrySet()) {
            //获取绑定键
            String bindingKey = bindingKeyEntry.getKey()String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME,bindingKey,null,message.getBytes("UTF-8"))System.out.println("生产者发出的消息:" + message)}
    }
}
6.2 消费者1
package com.lisa.rabitmq.topicimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 发布订阅模式——topic
 * 消费者1
 *
 */
public class Consumer05 {
    public static final String EXCHANGE_NAME = "Topic2"public static final String QUEUE_NAME = "Q1"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明队列
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//绑定队列到交换机  绑定到error
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.orange.*")System.out.println("消费者1等待接收消息,把接收到的消息打印在屏幕........... ")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()"UTF-8").toString();

            m = "接收的队列是:"+QUEUE_NAME + "接收绑定键:" + message.getEnvelope().getRoutingKey()+"  消息: "+m.toString()System.out.println("消费者1接收到的消息:" )System.out.println(m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}
6.3 消费者2
package com.lisa.rabitmq.topicimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.CancelCallbackimport com.rabbitmq.client.Channelimport com.rabbitmq.client.DeliverCallbackimport com.rabbitmq.client.Delivery/**
 * 发布订阅模式——direct
 * 消费者1
 *
 */
public class Consumer06 {
    public static final String EXCHANGE_NAME = "Topic2"public static final String QUEUE_NAME = "Q2"public static void main(String[] args) throws Exception{
        Channel channel = RabbitMQUtils.getChannel()//声明队列
        channel.queueDeclare(QUEUE_NAME,falsefalsefalsenull)//绑定队列到交换机  绑定到error
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.*.rabbit");
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"lazy.#")System.out.println("消费者2等待接收消息........... ")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()"UTF-8").toString();

            m = "接收的队列是:"+QUEUE_NAME + "接收绑定键:" + message.getEnvelope().getRoutingKey()+"  消息: "+m.toString()System.out.println("消费者2 控制台打印接收到的消息:")System.out.println(m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}

六、死信队列

1、死信的概念

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

应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生

异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时

自动失效

2、死信的来源

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

3、死信实战

3.1 代码架构图

RabbitMQ学习_第48张图片

3.2 消 息 TTL 过期
消费者01
package com.lisa.rabitmq.deadQueueimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.*import java.util.HashMapimport java.util.Map/**
 * 消费者01
 * 死信队列实战
 */
public class Consumer01 {

    //普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange"//死信交换机
    public static final String DEAD_EXCHANGE = "dead_exchange"//普通队列
    public static final String NORMAL_QUEUE = "normal_queue"//死信队列
    public static final String DEAD_QUEUE = "dead_queue"public static void main(String[] args) throws Exception {
        Channel channel = RabbitMQUtils.getChannel()//声明普通交换机和死信交换机 为direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT)//声明死信队列
        channel.queueDeclare(DEAD_QUEUE,falsefalsefalsenull)//绑定死信交换机与死信队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi")//声明普通队列
        Map<StringObject> arguments = new HashMap<>()//声明过期时间  也可以在生产方指定过期时间
        //arguments.put("x-message=ttl",100000);
        //正常队列设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE)//设置死信队列routing-key
        arguments.put("x-dead-letter-routing-key""lisi")//arguments:用于正常队列与死信队列之间的绑定
        channel.queueDeclare(NORMAL_QUEUE,falsefalsefalse,arguments)//绑定普通交换机与普通队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan")System.out.println("C1等待接收消息:")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()"UTF-8").toString()System.out.println("消费者1接收到的消息:"+m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback)}
}

消费者01启动:

后台显示:

RabbitMQ学习_第49张图片

队列显示:

RabbitMQ学习_第50张图片

交换机显示:

RabbitMQ学习_第51张图片

消费者01启动之后,停止

RabbitMQ学习_第52张图片

生产者:
package com.lisa.rabitmq.deadQueueimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeTypeimport com.rabbitmq.client.Channel/**
 * 死信队列实战
 * 生产者
 */
public class Producer01 {
    //普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange"public static void main(String[] args) throws Exception {
        Channel channel = RabbitMQUtils.getChannel()//死信消息设置TTL 时间
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build()for (int i = 0; i < 10; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes())System.out.println("生产者发送消息:"+message)}
    }
}

生产者发送10条消息:显示正常队列中有10条消息。

RabbitMQ学习_第53张图片

10秒之后,消息到达了死信队列中:

RabbitMQ学习_第54张图片

消费者02:
package com.lisa.rabitmq.deadQueueimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.*/**
 * 死信队列实战
 * 消费者02
 */
public class Consumer02 {
    //死信队列
    public static final String DEAD_QUEUE = "dead_queue"public static void main(String[] args) throws Exception {
        Channel channel = RabbitMQUtils.getChannel()System.out.println("消费者02等待接收死信队列消息。。。。。")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            String m = new String(message.getBody()"UTF-8").toString()System.out.println("消费者2接收到死信队列的消息:"+m)}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")};
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback)}
}

启动消费者02后,死信队列中的消息被消费:

RabbitMQ学习_第55张图片

3.3 队列达到最大长度

修改生产代码:取消TTL过期时间:

package com.lisa.rabitmq.deadQueueimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeTypeimport com.rabbitmq.client.Channel/**
 * 死信队列实战
 * 生产者
 */
public class Producer01 {
    //普通交换机
    public static final String NORMAL_EXCHANGE = "normal_exchange"public static void main(String[] args) throws Exception {
        Channel channel = RabbitMQUtils.getChannel()//死信消息设置TTL 时间
        //AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        //channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);

        for (int i = 0; i < 10; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE,"zhangsan"null,message.getBytes())System.out.println("生产者发送消息:"+message)}
    }
}

在消费者01中添加正常队列的最大长度限制:

//设置正常队列长度的限制
arguments.put("x-max-length"6)

消费者02代码不变:

测试:

首先删除原先的正常队列,因为参数变了

然后启动消费者01后,停止

RabbitMQ学习_第56张图片

启动生产者:

RabbitMQ学习_第57张图片

可以看出正常队列最多消费6条消息,其余的消息放到了死信队列

启动消费者02后,看到死信队列中的消息被消费了

RabbitMQ学习_第58张图片

3.4 消息被拒
  1. 消息生产者代码同上生产者一致

  2. C1 消费者代码

    关闭队列最大长度限制

    如果消息是info5就被拒绝

    package com.lisa.rabitmq.deadQueueimport com.lisa.rabitmq.utils.RabbitMQUtilsimport com.rabbitmq.client.*import java.util.HashMapimport java.util.Map/**
     * 消费者01
     * 死信队列实战
     */
    public class Consumer01 {
    
        //普通交换机
        public static final String NORMAL_EXCHANGE = "normal_exchange"//死信交换机
        public static final String DEAD_EXCHANGE = "dead_exchange"//普通队列
        public static final String NORMAL_QUEUE = "normal_queue"//死信队列
        public static final String DEAD_QUEUE = "dead_queue"public static void main(String[] args) throws Exception {
            Channel channel = RabbitMQUtils.getChannel()//声明普通交换机和死信交换机 为direct
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT)//声明死信队列
            channel.queueDeclare(DEAD_QUEUE,falsefalsefalsenull)//绑定死信交换机与死信队列
            channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi")//声明普通队列
            Map<StringObject> arguments = new HashMap<>()//声明过期时间  也可以在生产方指定过期时间
            //arguments.put("x-message=ttl",100000);
            //正常队列设置死信队列
            arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE)//设置死信队列routing-key
            arguments.put("x-dead-letter-routing-key""lisi")//设置正常队列长度的限制
            //arguments.put("x-max-length",6);
    
            channel.queueDeclare(NORMAL_QUEUE,falsefalsefalse,arguments)//绑定普通交换机与普通队列
            channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan")System.out.println("C1等待接收消息:")//推送的消息如何进行消费的接口回调
            DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
                String m = new String(message.getBody()"UTF-8").toString()if(m.equals("info5")){
                    System.out.println("消费者1接收到的消息:"+m + "此消息是被C1拒绝的")//拒绝消息:(消息序号,是否放回原队列中)
                    //requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
                    channel.basicReject(message.getEnvelope().getDeliveryTag()false)}else{
                    System.out.println("消费者1接收到的消息:"+m);
                    channel.basicAck(message.getEnvelope().getDeliveryTag()false)}
            }//取消消费的一个回调接口 如在消费的时候队列被删除掉了
            CancelCallback cancelCallback = (String consumerTag) ->{
                System.out.println("消息消费被中断!")}//开启手动应答
      channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,cancelCallback)}
    }
    

    RabbitMQ学习_第59张图片

  3. C2 消费者代码不变
    启动消费者 1 然后再启动消费者 2

RabbitMQ学习_第60张图片

进入死信队列中去,看到就是消息info5

RabbitMQ学习_第61张图片

启动消费者2:消费掉死信队列中拒绝的消息

image-20211129140325852

七、延迟队列

延迟队列就是死信队列的一种,是消息TTL过期

7.1. 延迟队列概念

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

7.2. 延迟队列使用场景

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

​ 这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:
发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎
使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果
数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,
如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支
付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十
分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万
级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单
的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

7.3. RabbitMQ 中的 TTL

TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有
消息的最大存活时间,

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

7.3.1. 消息设置TTL

另一种方式便是针对每条消息设置TTL

7.4RabbitMQ 整合SpringBoot

1、创建项目

RabbitMQ学习_第62张图片

2、导入依赖
<parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.2.RELEASEversion>
        <relativePath/> 
    parent>

    <groupId>org.examplegroupId>
    <artifactId>springboot-rabbitmqartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.47version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
        
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.9.2version>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>2.9.2version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.amqpgroupId>
            <artifactId>spring-rabbit-testartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>

    dependencies>
    <build>
        <plugins>
            
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
3、添加配置文件

application.yml

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
4、Swagger配置类
package com.example.springbootRabbitmq.configimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport springfox.documentation.builders.ApiInfoBuilderimport springfox.documentation.service.ApiInfoimport springfox.documentation.service.Contactimport springfox.documentation.spi.DocumentationTypeimport springfox.documentation.spring.web.plugins.Docketimport springfox.documentation.swagger2.annotations.EnableSwagger2@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApi())
                .select()
                .build()}
    public ApiInfo webApi(){
        return new ApiInfoBuilder()
                .title("rabbitmq 接口文档")
                .description("本文档描述了 rabbitmq 微服务接口定义")
                .version("1.0")
                .contact(new Contact("lisa""http://baidu.com""[email protected]"))
                .build()}
}
5、启动类
package com.example.springbootRabbitmqimport org.springframework.boot.SpringApplicationimport org.springframework.boot.autoconfigure.SpringBootApplication@SpringBootApplication
public class SpringBootRabbitmq {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootRabbitmq.class,args)}
}

7.5 队列TTL

1、代码架构图

创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后再创建一个交换机 X 和死信交

换机 Y,它们的类型都是direct,创建一个死信队列 QD,它们的绑定关系如下:

RabbitMQ学习_第63张图片

P生产者发送消息C,C只要在QD 队列中收消息即可

不管是10s的消息还是40s的消息,都会发送到QD(死信队列)

2、配置文件类代码
package com.example.springbootRabbitmq.configimport com.rabbitmq.client.AMQP;
import org.springframework.amqp.core.*import org.springframework.beans.factory.annotation.Qualifierimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport java.util.HashMapimport java.util.Map/**
 * TTL队列 配置文件
 */
@Configuration
public class TTLQueueConfig {
    //普通交换机
    public static final String X_EXCHANGE = "X"//死信交换机
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y"//普通队列
    public static final String QUEUE_A = "QA"public static final String QUEUE_B = "QB"//死信队列
    public static final String DEAD_LETTER_QUEUE = "QD"//声明普通X交换机
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE)}
    //声明死信Y交换机
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE)}
    //声明普通队列A并绑定死信队列  TTL 10s
    @Bean("queueA")
    public Queue queueA(){
        Map<StringObject> arguments = new HashMap<>()//声明当前队列绑定的死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE)//声明当前队列的死信路由 key
        arguments.put("x-dead-letter-routing-key""YD")//声明队列的 TTL  10s
        arguments.put("x-message-ttl"10000)return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build()}
    //声明普通队列B并绑定死信队列  TTL 40s
    @Bean("queueB")
    public Queue queueB(){
        Map<StringObject> arguments = new HashMap<>()//声明当前队列绑定的死信交换机
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE)//声明当前队列的死信路由 key
        arguments.put("x-dead-letter-routing-key""YD")//声明队列的 TTL  10s
        arguments.put("x-message-ttl"40000)return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build()}
    //声明死信队列
    @Bean("queueD")
    public Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build()}
    //队列QA绑定交换机X
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange){
        //                           队列QA     X交换机       routing key
        return BindingBuilder.bind(queueA).to(xExchange).with("XA")}
    //队列QB绑定交换机X
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,@Qualifier("xExchange") DirectExchange xExchange){
        //                           队列QA     X交换机       routing key
        return BindingBuilder.bind(queueB).to(xExchange).with("XB")}
    //队列QD绑定交换机Y
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange){
        //                           队列QA     X交换机       routing key
        return BindingBuilder.bind(queueD).to(yExchange).with("YD")}
}
3、生产者

生产者发送消息,主要是在Controller中发现两条消给两条普通队列

package com.example.springbootRabbitmq.controllerimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.rabbit.core.RabbitTemplateimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.PathVariableimport org.springframework.web.bind.annotation.RequestMappingimport org.springframework.web.bind.annotation.RestControllerimport java.util.Date/**
 * 生产者
 * 发送延迟消息
 *  http://localhost:8080/ttl/sendMsg/嘻嘻嘻
 */
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMessage {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMsg/{message}")
    public String sendMsg(@PathVariable String message){
        log.info("当前时间:{},发送一条信息给两个 TTL 队列,消息为:{}"new Date(), message);
        rabbitTemplate.convertAndSend("X""XA""消息来自ttl为10s的队列:"+message);
        rabbitTemplate.convertAndSend("X""XB""消息来自ttl为40s的队列:"+message)return "发送消息OK"}

}
4、消费者

所有的消息都会转到死信队列中,消费者只要在死信队列中接收消息即可

package com.example.springbootRabbitmq.consumerimport com.rabbitmq.client.Channelimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.core.Messageimport org.springframework.amqp.rabbit.annotation.RabbitListenerimport org.springframework.stereotype.Componentimport java.io.UnsupportedEncodingExceptionimport java.util.Date/**
 * 死信队列ttl
 * 消费者
 */
@Component
@Slf4j
public class DeadLetterConsumer {

    //监听死信队列QD 
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel) throws UnsupportedEncodingException {
        String msg = new String(message.getBody()"UTF-8").toString();

        log.info("当前时间:{},收到死信队列信息为{}"new Date().toString(),msg)}
}
5、启动测试

image-20211130135621459

观察时间,一个相差10s的消息,一个相差40s的消息

6、出现的错误
没有DirectExchange这个类
org.springframework.amqp.core.DirectExchange;即这个包没有

解决办法:提升spring-boot-starter-parent的版本

之前用的是2.2.2 改为2.3.11后可行

第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S两个时间选项,如果需要一个小时后处理,那么就需要增加TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?

7.6 延迟队列优化

7.6.1 代码架构图

在这里新增了一个队列 QC,绑定关系如下,该队列不设置TTL 时间,主要是通过生产者在发消息时来指定所需的时间

image-20211130140350903

7.6.2 配置文件
@Configuration
public class TTLQueueConfig {
     //延迟队列优化
    public static final String QUEUE_C = "QC"//延迟队列优化:声明队列C
    @Bean("queueC")
    public Queue queueC(){
        Map<StringObject> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        arguments.put("x-dead-letter-routing-key""YD")return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build()}
     //队列QC绑定交换机X
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,@Qualifier("xExchange") DirectExchange xExchange){
        //                           队列QA     X交换机       routing key
        return BindingBuilder.bind(queueC).to(xExchange).with("XC")}
}
7.6.3 生产者
//延迟队列优化
@GetMapping("sendMsg/{message}/{ttlTime}")
public String sendMsg2(@PathVariable String message,@PathVariable String ttlTime){
    log.info("当前时间:{},发送一条时长{}毫秒TTL信息给队列C,消息为:{}"new Date(), ttlTime,message);
    rabbitTemplate.convertAndSend("X""XC",message,correlationData->{
        correlationData.getMessageProperties().setExpiration(ttlTime)return correlationData;
    })return "延迟队列优化发送消息Ok"}

源码:

//  交换机  routingkey 消息  messagePostProcessor
@Override
public void convertAndSend(String exchange, String routingKey, final Object message,
      final MessagePostProcessor messagePostProcessor) throws AmqpException {
   convertAndSend(exchange, routingKey, message, messagePostProcessor, null)}

MessagePostProcessor

@FunctionalInterface
public interface MessagePostProcessor {
   Message postProcessMessage(Message message) throws AmqpException;
   。。。
}
7.6.4 启动测试
http://localhost:8080/ttl/sendMsg/你好1/20000
http://localhost:8080/ttl/sendMsg/你好2/2000

RabbitMQ学习_第64张图片

问题:

看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消
息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,
如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

7.7. Rabbitmq 插件实现延迟队列

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

7.7.1 安装延时队列插件

在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载

rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。

方法一:

进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

方法二:使用Docker安装的RabbitMQ使用这个方法:

RabbitMQ学习_第65张图片

RabbitMQ学习_第66张图片

# 容器外面复制插件到容器内部的/plugins目录下:b539e7f05ea2:是容器ID
docker cp /home/下载/rabbitmq_delayed_message_exchange-3.9.0.ez b539e7f05ea2:/plugins
# 进入容器内部 查看插件是否放进去
$ docker exec -it myrabbitmq /bin/bash
root@b539e7f05ea2:/plugins# ls

image-20211130152829701

启用插件

命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange

然后重启容器

# 进入容器内部的/plugins目录下
$ docker exec -it myrabbitmq /bin/bash
root@b539e7f05ea2:/# cd /plugins
root@b539e7f05ea2:/plugins# rabbitmq-plugins enable rabbitmq_delayed_message_exchange                 # 启用插件
Enabling plugins on node rabbit@b539e7f05ea2:
rabbitmq_delayed_message_exchange
The following plugins have been configured:
  rabbitmq_delayed_message_exchange
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_prometheus
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@b539e7f05ea2...
The following plugins have been enabled:
  rabbitmq_delayed_message_exchange

started 1 plugins.
root@b539e7f05ea2:/plugins# exit  # 退出容器
$ docker restart myrabbitmq    #重启容器
myrabbitmq

可以看到延迟插件生效:

RabbitMQ学习_第67张图片

7.7.2 代码架构图

RabbitMQ学习_第68张图片

7.7.3 配置类

RabbitMQ学习_第69张图片

package com.example.springbootRabbitmq.configimport org.springframework.amqp.core.*import org.springframework.beans.factory.annotation.Qualifierimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport java.util.HashMapimport java.util.Map/**
 * Rabbitmq 插件实现延迟队列
 */
@Configuration
public class DelayedQueueConfig {
    //延迟交换机
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange"//队列
    public static final String DELAYED_QUEUE_NAME = "delayed.queue"//routingkey
    public static final String DELAYED_ROUTING_KEY_NAME = "delayed.routingkey"//声明交换机  自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange(){
        Map<StringObject> arguments = new HashMap<>()//自定义交换机的类型
        arguments.put("x-delayed-type""direct")//CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map arguments)
        return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message"truefalse,arguments)}

    //声明队列
    @Bean
    public Queue delayedQueue(){
        return QueueBuilder.durable(DELAYED_QUEUE_NAME).build()//return new Queue(DELAYED_QUEUE_NAME);
    }
    //队列与交换机的绑定
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,@Qualifier("delayedExchange") CustomExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY_NAME).noargs()}
}
7.7.4 生产者
//延迟队列优化:基于插件
@GetMapping("sendDelayMsg/{message}/{delayedTime}")
public String sendDelayMsg(@PathVariable String message,@PathVariable Integer delayedTime){
    log.info("当前时间:{},发送一条延迟{}毫秒信息给队列delayed.queue,消息为:{}"new Date(), delayedTime,message);
    rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY_NAME,message, correlationData->{
        correlationData.getMessageProperties().setDelay(delayedTime)return correlationData;
    })return "延迟队列优化:基于插件发送消息Ok"}
7.7.5 消费者
package com.example.springbootRabbitmq.consumerimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.core.Messageimport org.springframework.amqp.rabbit.annotation.RabbitListenerimport org.springframework.stereotype.Componentimport java.util.Date/**
 * 延迟队列:基于插件
 * 消费者
 */
@Component
@Slf4j
public class DelayedConsumer {
    //队列
    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());
        log.info("当前时间:{},收到延时队列信息为{}"new Date().toString(),msg)}
}
7.7.6 测试

访问:

http://localhost:8080/ttl/sendDelayMsg/你好1/20000
http://localhost:8080/ttl/sendDelayMsg/你好2/2000

image-20211130163446005

第二个消息被先消费掉了,符合预期

7.8. 总结

延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。当然,延时队列还有很多其它选择,比如利用 Java DelayQueue,利用 Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

八、 发布确认高级

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

8.1. 发布确认 springboot 版本

8.1.1 确认机制方案
8.1.2 代码架构图

image-20211201093858845

8.1.3 配置类

主要是声明交换机、队列以及绑定队列与交换机

package com.example.springbootRabbitmq.configimport org.springframework.amqp.core.*import org.springframework.beans.factory.annotation.Qualifierimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configuration/**
 * 发布确认高级
 */
@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange"public static final String CONFIRM_QUEUE_NAME = "confirm.queue"public static final String CONFIRM_ROUTING_KEY = "key1"//声明交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return new DirectExchange(CONFIRM_EXCHANGE_NAME)}
    //声明队列
    @Bean("confirmQueue")
    public Queue confirmQueue(){
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build()}
    //绑定
    @Bean
    public Binding bindingQueueExchange(@Qualifier("confirmQueue") Queue confirmQueue,@Qualifier("confirmExchange") DirectExchange confirmExchange){
        return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY)}
}
8.1.4 消息生产者

以下是正常消息发送

package com.example.springbootRabbitmq.controllerimport com.example.springbootRabbitmq.config.ConfirmConfigimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.rabbit.core.RabbitTemplateimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.PathVariableimport org.springframework.web.bind.annotation.RequestMappingimport org.springframework.web.bind.annotation.RestController/**
 * 发布确认高级——消息生产者
 */
@Slf4j
@RestController()
@RequestMapping("/confirm")
public class ConfirmController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendConfirmMsg/{message}")
    public String sendConfirmMsg(@PathVariable String message){
        rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message);
        log.info("生产者发送消息的内容:{}",message)return "发送消息成功!"}
}
8.1.5 消息消费者

主要是监听队列中的消息,接收消息

package com.example.springbootRabbitmq.consumerimport com.example.springbootRabbitmq.config.ConfirmConfigimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.core.Messageimport org.springframework.amqp.rabbit.annotation.RabbitListenerimport org.springframework.stereotype.Component/**
 * 发送确认高级——消费者
 */
@Slf4j
@Component
public class ConfirmConsumer {
    @RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
    public void receive(Message message){
        String msg = new String(message.getBody());
        log.info("消费者收到的消息为:{}",msg)}
}
8.1.6 测试1

访问:http://localhost:8080/confirm/sendConfirmMsg/AAA1

RabbitMQ学习_第70张图片

发送消息、接收消息正常!

8.1.7 回调接口

当交换机接收不到消息,就需要一个回调接口将当前的消息保存下来,以便日后发送消息

package com.example.springbootRabbitmq.configimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.rabbit.connection.CorrelationDataimport org.springframework.amqp.rabbit.core.RabbitTemplateimport org.springframework.stereotype.Component/**
 * 回调接口
 */
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
    /**
     * 交换机不管是否接收到消息都会触发这个回调接口
     * ack=true:交换机接收到消息
     * ack=false:交换机未接收到消息
     * 
     * @param correlationData  保存回调消息的ID及相关信息  
     * @param ack         是否接收到消息
     * @param cause       失败的原因,若ack=true,cause=null
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : ""if (ack){
            log.info("交换机已经接收到 id 为:{}的消息",id)}else{
            log.info("交换机已经接收到 id 为:{}的消息,原因是:{}",id,cause)}
    }
}

要想让这个回调接口生效,就需要在配置文件中进行配置

8.1.8 修改配置文件

在配置文件当中需要添加

spring.rabbitmq.publisher-confirm-type=correlated

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

经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,

其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法

等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是

waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker

application.yml

spring:
  rabbitmq:
    host: 172.17.0.2
    port: 5672
    username: admin
    password: admin
    publisher-confirm-type: correlated   #发布确认模式
8.1.9 测试2

修改生产者,设置回调消息的ID

@GetMapping("/sendConfirmMsg/{message}")
public String sendConfirmMsg(@PathVariable String message){
    //设置回调消息的ID,然后放入发送消息的方法中
    CorrelationData correlationData = new CorrelationData("1"); rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
    log.info("生产者发送消息的内容:{}",message)return "发送消息成功!"}

启动测试:可以看到交换机已经接收到消息了

image-20211201110355881

模拟交换机出错,接收不到消息:修改交换机

//写错交换机的名字
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+1ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData)

再次测试:

生产者发送消息的内容:AAA1
2021-12-01 11:06:53.127 ERROR 1113286 --- [172.17.0.2:5672] o.s.a.r.c.CachingConnectionFactory       : Channel shutdown: channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange1' in vhost '/', class-id=60, method-id=40)
2021-12-01 11:06:53.128  INFO 1113286 --- [nectionFactory2] c.e.s.config.MyCallBack                  : 交换机没有接收到 id 为:1的消息,原因是:channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange1' in vhost '/', class-id=60, method-id=40)

image-20211201110746642

模拟队列接收不到消息:修改routingkey

@GetMapping("/sendConfirmMsg/{message}")
public String sendConfirmMsg(@PathVariable String message){
    //设置回调消息的ID,然后放入发送消息的方法中
    CorrelationData correlationData1 = new CorrelationData("1");
    rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData1)CorrelationData correlationData2 = new CorrelationData("2")//模拟队列接收不到消息
    rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY+1,message,correlationData2);
    log.info("生产者发送消息的内容:{}",message)return "发送消息成功!"}

RabbitMQ学习_第71张图片

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

8.2. 消息无法接收回退消息

​ 在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如
果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何
让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。

方法有两种:

方法一:

application.yml中设置spring.rabbitmq.publisher-returns: true

方法二:

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

spring:
  rabbitmq:
    host: 172.17.0.2
    port: 5672
    username: admin
    password: admin
    publisher-confirm-type: correlated   #发布确认模式
    publisher-returns: true   #消息路由失败回退消息给生产者
8.2.1 修改回调接口

如果在application.yml中设置了spring.rabbitmq.publisher-returns: true就不必设置Mandatory为true,二者效果一致

import javax.annotation.PostConstruct/**
 * 回调接口
 */
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallbackRabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    //[注入]把当前写的这个类注入到RabbitTemplate中去
    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this)/**
        [二选一]:如果在application.yml中设置了spring.rabbitmq.publisher-returns: true就不必设置Mandatory为true
         * true:交换机无法将消息进行路由时,会将该消息返回给生产者
         * false:如果发现消息无法进行路由,则直接丢弃
         */
        rabbitTemplate.setMandatory(true)//设置回退消息交给谁处理
        rabbitTemplate.setReturnCallback(this)}
    /**
     * 可以在当消息传递过程中不可达目的地时将消息返回给生产者。
     * 只有在不可达时才进行回调
     * @param message    消息
     * @param replyCode  响应码
     * @param replyText  响应内容
     * @param exchange   交换机
     * @param routingKey 路由
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {

        log.info("消息:{},被交换机 :{}退出,退回原因 {},路由key:{}"new String(message.getBody()),exchange,replyText,routingKey)}
}
8.2.2 消息生产者代码

生产者代码保持上面8.1.9中测试2的队列接收不到消息的场景:

RabbitMQ学习_第72张图片

8.2.3 测试

RabbitMQ学习_第73张图片

生产者发送消息CCC,两个交换机已经收到消息,消费者只有收到一条消息,另一条消息被退回

8.3. 备份交换机

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

8.3.1 代码架构图

RabbitMQ学习_第74张图片

生产者发送两条消息,一条可以被[确认消费者]收到,另一条由于不可路由,该消费者不能收到。所以创建出一个备份交换机,来接收交换机不可路由的消息,然后将消息发送给备份队列和警告队列,然后用独立的消费者进行监测和警告

8.3.2 修改配置类
/**
 * 发布确认高级
 */
@Configuration
public class ConfirmConfig {
    //1、备份交换机、备份队列、警告队列
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange"public static final String BACKUP_QUEUE_NAME = "backup.queue"public static final String WARNING_QUEUE_NAME = "warning.queue"//2、声明备份交换机
    @Bean("backupExchange")
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME)}
    //3、声明备份队列
    @Bean("backupQueue")
    public Queue backupQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build()}
    //4、绑定
    //备份队列绑定备份交换机
    @Bean
    public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,@Qualifier("backupExchange") FanoutExchange backupExchange){
        return BindingBuilder.bind(backupQueue).to(backupExchange)}
    //警告队列绑定备份交换机
    @Bean
    public Binding warningQueueBindingBackupExchange(@Qualifier("warningQueue") Queue warningQueue,@Qualifier("backupExchange") FanoutExchange backupExchange){
        return BindingBuilder.bind(warningQueue).to(backupExchange)}
    //5、确认交换机与备份交换机的绑定
    //声明确认交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange(){
        return ExchangeBuilder.directExchange(CONFIRM_QUEUE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build()//return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }
}

由于确认交换机已经改变,所以应该删除后再启动,否则会报错

#method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'type' for exchange 'backup.exchange' in vhost '/': received 'fanout' but current is 'direct', class-id=40, method-id=10)
8.3.3 警告消费者
package com.example.springbootRabbitmq.consumerimport com.example.springbootRabbitmq.config.ConfirmConfigimport lombok.extern.slf4j.Slf4jimport org.springframework.amqp.core.Messageimport org.springframework.amqp.rabbit.annotation.RabbitListenerimport org.springframework.stereotype.Component/**
 * 警告消费者
 */
@Component
@Slf4j
public class WarningConsumer {

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

}
8.3.4 测试

RabbitMQ学习_第75张图片

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

九、 RabbitMQ 其他知识点

9.1. 幂等性

9.1.1 概念

​ 用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

​ 举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是在响应客户端的时候也有可能出现网络中断或者异常等等

9.1 .2 消息重复消费

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

9.1.3解决思路

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

9.1.4 消费端的幂等性保障

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

业界主流的幂等性有两种操作:

  • a.唯一 ID+指纹码机制,利用数据库主键去重,
  • b.利用 redis 的原子性去实现
9.1.5 唯 一 ID+指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中

  • 优势就是实现简单就一个拼接,然后查询判断是否重复;

  • 劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

9.1.6 Redis 原子性

利用 redis 执行setnx 命令,天然具有幂等性。从而实现不重复消费

9.2. 优先级队列

9.2.1 使用场景

​ 在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以**订单量大了后采用 RabbitMQ 进行改造和优化,**如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。

9.2.2 如何添加

a. 控制台页面添加

RabbitMQ学习_第76张图片

b. 队列中代码添加优先级

HashMap<StringObject> params = new HashMap<>();
params.put("x-max-priority"10)//优先级的区间时0-255,这里设置队列的优先级是10
channel.queueDeclare(QUEUE_NAME,truefalsefalse,params)

c. 消息中代码添加优先级

AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes())

注意:要让队列实现优先级需要做的事情有如下事情:

  • 队列需要设置为优先级队列
  • 消息需要设置消息的优先级
  • **消费者需要等待消息已经发送到队列中才去消费因为,**这样才有机会对消息进行排序
9.2.3 实战

生产者

package com.lisa.rabitmq.oneimport com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channelimport com.rabbitmq.client.Connectionimport com.rabbitmq.client.ConnectionFactoryimport java.io.IOExceptionimport java.util.HashMapimport java.util.concurrent.TimeoutException/**
 * 生产者
 */
public class Producer {
    private static final String QUEUE_NAME = "hello"public static void main(String[] args) throws IOExceptionTimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin")Connection connection = factory.newConnection()//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Channel channel = connection.createChannel()/**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        HashMap<StringObject> params = new HashMap<>();
        params.put("x-max-priority"10)//优先级的区间时0-255,这里设置队列的优先级是10
        channel.queueDeclare(QUEUE_NAME,truefalsefalse,params)for (int i = 1; i < 11; i++) {
            String message = "hello Lisa "+ i;
            //设置消息的优先级:当i=5时,设置该消息的优先级为5,其他的消息优先级默认
            if(i == 5){
                AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
                channel.basicPublish("",QUEUE_NAME,properties,message.getBytes())}else{
                //默认优先级
                channel.basicPublish("",QUEUE_NAME,null,message.getBytes())}
        }

        /**
         * 发送一个消息
         * 1.发送到那个交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
        //channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("消息发送完毕!")}
}

消费者

package com.lisa.rabitmq.oneimport com.rabbitmq.client.*import java.io.IOExceptionimport java.util.concurrent.TimeoutException/**
 * 消费者
 */
public class Customer {
    private static final String QUEUE_NAME = "hello"public static void main(String[] args) throws IOExceptionTimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("admin");
        factory.setPassword("admin")Connection connection = factory.newConnection()//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Channel channel = connection.createChannel()System.out.println("等待接收消息!")//推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
            System.out.println(new String(message.getBody()).toString())}//取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback = (String consumerTag) ->{
            System.out.println("消息消费被中断!")}/**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
         * 3.消费者未成功消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback)}
}

测试

先启动生产者:

RabbitMQ学习_第77张图片

image-20211202092615438

再启动消费者:

RabbitMQ学习_第78张图片

可以看到第5条消息被优先消费

9.3. 惰性队列

9.3.1 使用场景

​ RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储

​ 当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

​ 默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,
这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留
一份备份。**当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,**这个操作会耗费较长的
时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

RabbitMQ学习_第79张图片

9.3.2 两种模式

队列具备两种模式:defaultlazy

默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。

lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。

​ 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示
例中演示了一个惰性队列的声明细节:

方式一:

RabbitMQ学习_第80张图片

方式二:

Map<StringObject> args = new HashMap<StringObject>();
args.put("x-queue-mode""lazy");
channel.queueDeclare("myqueue"falsefalsefalse, args)
9.3.3 内存开销对比

RabbitMQ学习_第81张图片

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅
占用 1.5MB。因为内存当中只剩下消息的索引,消息的内容存放到了磁盘中。

十、 RabbitMQ 集群

10.1. clustering

10.1.1 使用集群的原因

最开始我们介绍了如何安装及运行 RabbitMQ 服务,不过这些是单机版的,无法满足目前真实应用的
要求。如果 RabbitMQ 服务器遇到内存崩溃、机器掉电或者主板故障等情况,该怎么办?单台 RabbitMQ
服务器可以满足每秒 1000 条消息的吞吐量,那么如果应用需要 RabbitMQ 服务满足每秒 10 万条消息的吞
吐量呢?购买昂贵的服务器来增强单机 RabbitMQ 务的性能显得捉襟见肘,搭建一个 RabbitMQ 集群才是
解决实际问题的关键.

本文介绍RabbitMQ搭建普通集群模式和镜像集群模式的操作指南。

10.1.2 内容

RabbitMQ有2种集群模式,分别是普通集群模式和镜像集群模式。

第一种 普通集群模式:rabbitmq集群与其他集群有些不同,rabbitmq集群同步的指是复制队列,元数据信息的同步,即同步的是数据存储信息;消息的存放只会存储在创建该消息队列的那个节点上。并非在节点上都存储一个完整的数据。在通过非数据所在节点获取数据时,通过元数据信息,路由转发到存储数据节点上,从而得到数据 。

第二种 镜像集群模式:与普通集群模式区别 主要是消息实体会主动在镜像节点间同步数据,而不是只存储数据元信息。 故普通集群模式 但凡数据节点挂了,容易造成数据丢失但镜像集群模式可以保证集群只要不全部挂掉,数据就不会丢失,当相对于性能来说,镜像集群模式会比普通集群模式多出消耗数据的传输。故取决于业务场景进行取舍。

10.1.2 普通集群模式
(1)拉取rabbitmq镜像
sudo docker pull rabbitmq:management
(2)创建映射数据卷目录
mkdir rabbitmqcluster
cd rabbitmqcluster/
mkdir rabbitmq01 rabbitmq02 rabbitmq03

创建完成映射目录后,执行如下命令创建容器:

(3)创建容器

使用的镜像rabbitmq:management创建容器

docker run -d --hostname rabbitmq01 --name rabbitmqCluster01 -v /home/dockertest/rabbitmqcluster/rabbitmq01:/var/lib/rabbitmq -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:management

docker run -d --hostname rabbitmq02 --name rabbitmqCluster02 -v /home/dockertest/rabbitmqcluster/rabbitmq02:/var/lib/rabbitmq -p 15673:15672 -p 5673:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie'  -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --link rabbitmqCluster01:rabbitmq01 rabbitmq:management

docker run -d --hostname rabbitmq03 --name rabbitmqCluster03 -v /home/dockertest/rabbitmqcluster/rabbitmq03:/var/lib/rabbitmq -p 15674:15672 -p 5674:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie'  -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --link rabbitmqCluster01:rabbitmq01 --link rabbitmqCluster02:rabbitmq02  rabbitmq:management

由于上面这个镜像创建容器出现了问题,所以使用下面的镜像创建容器

使用rabbitmq:3.8.0-beta.4-management创建容器

docker run -d --hostname rabbitmq01 --name rabbitmqCluster01 -p 5672:5672 -p 15672:15672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' rabbitmq:3.8.0-beta.4-management

docker run -d --hostname rabbitmq02 --name rabbitmqCluster02 -p 5673:5672 -p 15673:15672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --link rabbitmqCluster01:rabbitmq01 rabbitmq:3.8.0-beta.4-management

docker run -d --hostname rabbitmq03 --name rabbitmqCluster03 -p 5674:5672 -p 15674:15672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --link rabbitmqCluster01:rabbitmq01 --link rabbitmqCluster02:rabbitmq02 rabbitmq:3.8.0-beta.4-management

使用docker ps 查看:

RabbitMQ学习_第82张图片

注:--hostname设置容器的主机名 RABBITMQ_ERLANG_COOKIE节点认证作用,部署集成时需要同步该值。

(4)访问

启动容器成功后,可以访问

http://localhost:15672/#/

http://localhost:15673/#/

http://localhost:15674/#/

查看是否正常启动成功。账号/密码:guest / guest。 读者登陆后,查看overview Tab页,可看到节点信息。

如果登录不进去,可以设置新用户并授予权限:

# 进去rabbitmqCluster01
docker exec -it rabbitmqCluster01 bash
# 添加用户admin
root@rabbitmq01:/# rabbitmqctl add_user admin admin
# 给admin授予权限
root@rabbitmq01:/# rabbitmqctl set_user_tags admin administrator
# 退出
root@rabbitmq01:/# exit
# 重启rabbitmqCluster01
/home/dockertest/rabbitmqcluster$ docker restart rabbitmqCluster01

rabbitmqCluster02 和rabbitmqCluster03执行相同的操作

再次访问

http://localhost:15672/#/

RabbitMQ学习_第83张图片

http://localhost:15673/#/

RabbitMQ学习_第84张图片

http://localhost:15674/#/

RabbitMQ学习_第85张图片

RabbitMQ学习_第86张图片

正常访问!

(5)容器节点加入集群

首先在centos窗口中,执行如下命令,进入第一个rabbitmq节点容器:

docker exec -it rabbitmqCluster01 bash

进入容器后,操作rabbitmq,执行如下命令:

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit

操作日志:

/home/dockertest/rabbitmqcluster$ docker exec -it rabbitmqCluster01 bash
root@rabbitmq01:/# rabbitctl stop_app
root@rabbitmq01:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbitmq01 ...
root@rabbitmq01:/# rabbitmq reset
root@rabbitmq01:/# rabbitmqctl reset
Resetting node rabbit@rabbitmq01 ...
root@rabbitmq01:/# rabbitmqctl start_app
Starting node rabbit@rabbitmq01 ...
root@rabbitmq01:/# exit
exit

接下来,设置节点2,加入到集群,执行如下命令:

docker exec -it myrabbit2 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
exit

操作日志:

/home/dockertest/rabbitmqcluster$ docker exec -it rabbitmqCluster02 bash
root@rabbitmq02:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbitmq02 ...
root@rabbitmq02:/# rabbitmqctl reset
Resetting node rabbit@rabbitmq02 ...
root@rabbitmq02:/# rabbitmqctl join_cluster --ram rabbit@rabbitmq01
Clustering node rabbit@rabbitmq02 with rabbit@rabbitmq01
root@rabbitmq02:/# 
root@rabbitmq02:/# rabbitmqctl start_app
Starting node rabbit@rabbitmq02 ...
 completed with 3 plugins.
root@rabbitmq02:/# 
root@rabbitmq02:/# exit
exit

设置节点3,加入到集群:

docker exec -it rabbit3 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq01
rabbitmqctl start_app
exit

guest guest

节点设置完成之后,在浏览器访问

http://localhost:15672/#/

http://localhost:15673/#/

http://localhost:15674/#/

中任意一个,都会看到RabbitMQ集群已经创建成功。

RabbitMQ学习_第87张图片

查看集群的状态:

$ docker exec -it rabbitmqCluster01 bash
root@rabbitmq01:/# rabbitmqctl cluster_status    #查看集群的状态
Cluster status of node rabbit@rabbitmq01 ...
Basics

Cluster name: rabbit@rabbitmq01

Disk Nodes

rabbit@rabbitmq01

RAM Nodes

rabbit@rabbitmq02
rabbit@rabbitmq03

Running Nodes

rabbit@rabbitmq01
rabbit@rabbitmq02
rabbit@rabbitmq03

Versions

rabbit@rabbitmq01: RabbitMQ 3.8.0-beta.4 on Erlang 22.0.5
rabbit@rabbitmq02: RabbitMQ  on Erlang 
rabbit@rabbitmq03: RabbitMQ  on Erlang 

Alarms

(none)

Network Partitions

(none)

Listeners

Node: rabbit@rabbitmq01, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@rabbitmq01, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@rabbitmq01, interface: [::], port: 15672, protocol: http, purpose: HTTP API

Feature flags

Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: quorum_queue, state: enabled
root@rabbitmq01:/# 

10.2. 镜像队列

10.2.1 使用镜像的原因

如果 RabbitMQ 集群中只有一个 Broker 节点,那么该节点的失效将导致整体服务的临时性不可用,并
且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable 属性也设置为true, 但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过 publisherconfirm 机制能够确保客户端知道哪些消息己经存入磁盘,尽管如此,一般不希望遇到因单点故障导致的服务不可用。

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

10.2.2 搭建步骤
  1. 启动三台集群节点
  2. 随便找一个节点添加 policy
  • 点击Admin的右侧的Policies

RabbitMQ学习_第88张图片

  • Add/Update a polies

RabbitMQ学习_第89张图片

  1. 在 node1 上创建一个队列发送一条消息,队列存在镜像队列,会在rabbit@rabbitmq02和rabbit@rabbitmq03中随机挑选
package com.lisa.rabitmq.oneimport com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channelimport com.rabbitmq.client.Connectionimport com.rabbitmq.client.ConnectionFactoryimport java.io.IOExceptionimport java.util.HashMapimport java.util.concurrent.TimeoutException/**
 * 生产者
 */
public class Producer {
    private static final String QUEUE_NAME = "mirror_hello"public static void main(String[] args) throws IOExceptionTimeoutException {
        //创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest")Connection connection = factory.newConnection()//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Channel channel = connection.createChannel()/**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        HashMap<StringObject> params = new HashMap<>();
        params.put("x-max-priority"10)//优先级的区间时0-255,这里设置队列的优先级是10
        channel.queueDeclare(QUEUE_NAME,truefalsefalse,params)/**
         * 发送一个消息
         * 1.发送到那个交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
        String message = "hello Lisa ";
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes())System.out.println("消息发送完毕!")}
}

RabbitMQ学习_第90张图片

可以看到node+1,点进去

RabbitMQ学习_第91张图片

  1. 停掉 node1 之后发现 node2 成为镜像队列
root@rabbitmq01:/# rabbitmqctl stop_app   #停止节点1
Stopping rabbit application on node rabbit@rabbitmq01 ...
root@rabbitmq01:/# 

可以看到第一个节点停止

RabbitMQ学习_第92张图片

查看队列,节点2升为主节点,节点3为备份节点

RabbitMQ学习_第93张图片

5.就算整个集群只剩下一台机器了 依然能消费队列里面的消息 说明队列里面的消息被镜像队列传递到相应机器里面了

你可能感兴趣的:(RabbirMQ学习,rabbitmq,java,分布式)