RabbitMQ

1.1. MQ 的相关概念

1.1.1. 什么是MQ

  1. MQ(message queue),M表示【消息】,Q表示【队列】,MQ就是【消息队列】;
  2. 消息队列:从字面意思上看,本质是个队列,只不过队列中存放的内容是 message 而已。
  3. 遵循先入先出原则(FIFO):好比运输水的水管,先流入水管的水,肯定也会先流出水管。
  4. 还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。

1.1.2. 为什么要用MQ ,也就是MQ的作用。

1.流量消峰

  例如秒杀活动,可能在短时间内会有很大请求同时到后端,如果后端对每个请求都执行业务操作,例如查询数据库和写数据库,会造成服务器压力过大,同时,在同一时间进行大量数据库操作,可能会出现数据异常,我们可以使用mq实现缓冲,将所有请求先放入消息队列中,服务端每次处理业务先从消息队列获取。

2.应用解耦

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

3.异步处理

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

1.1.3. MQ 的分类

1.ActiveMQ

  1. 优点:单机吞吐量万级,时效性 ms 级,可用性高,基于主从架构实现高可用性,消息可靠性较低的概率丢失数据。
  2. 缺点:官方社区现在对 ActiveMQ 5.x 维护越来越少,高吞吐量场景较少使用。

2.Kafka

  1. 介绍:大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开 Kafka,这款为大数据而生的消息中间件,以其百万级 TPS 的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥着举足轻重的作用。目前已经被 LinkedIn,Uber, Twitter, Netflix 等大公司所采纳。
  2. 优点: 性能卓越,单机写入 TPS 约在百万条/秒,最大的优点,就是吞吐量高。时效性 ms 级可用性非常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用,消费者采用 Pull 方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方KafkaWeb 管理界面 Kafka-Manager;在日志领域比较成熟,被多家公司和多个开源项目使用;功能支持: 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用。
  3. 缺点:Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;支持消息顺序,但是一台代理宕机后,就会产生消息乱序,社区更新较慢

3.RocketMQ

  1. 介绍:RocketMQ 出自阿里巴巴的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog 分发等场景。
  2. 优点:单机吞吐量十万级,可用性非常高,分布式架构,消息可以做到 0 丢失,MQ 功能较为完善,还是分布式的,扩展性好,支持 10 亿级别的消息堆积,不会因为堆积导致性能下降,源码是 java 我们可以自己阅读源码,定制自己公司的 MQ。
  3. 缺点:支持的客户端语言不多,目前是 java 及 c++,其中 c++不成熟;社区活跃度一般,没有在MQ核心中去实现 JMS 等接口,有些系统要迁移需要修改大量代码。

4.RabbitMQ

  1. 介绍:2007 年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一
  2. 优点:由于 erlang 语言的高并发特性,性能较好;吞吐量到万级,MQ 功能比较完备,健壮、稳定、易用、跨平台、支持多种语言 如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持 AJAX 文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高。网址:https://www.rabbitmq.com/news.html
  3. 缺点:商业版需要收费,学习成本较高。

1.1.4. MQ 的选择

  1. Kafka
      Kafka 主要特点是基于Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka 了。
  2. RocketMQ
      天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。
  3. RabbitMQ
      结合 erlang 语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。

1.2. RabbitMQ

1.2.1. RabbitMQ 的概念

  RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件,而是接收、存储和转发消息数据。

1.2.2. RabbitMQ 的组成部分

  1. 生产者
      产生数据并发送消息的程序是由生产者来完成。

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

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

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

1.2.3. 各个名词介绍

在这里插入图片描述

  1. Producer(生产者)
      产生数据并发送消息的程序是由生产者来完成。

  2. ConnectionFactory(连接工厂)
       用于建立连接的工厂。

  3. Connection(连接)
       用于生产者和消费者与RabbitMQ服务器建立TCP连接的通道。

  4. Channel(信道/通道)
       仅仅是建立了客户端到RabbitMQ服务器之间的连接,客户端仍不能发送/接收消息的。须为每个Connection(连接)建立Channel(信道),AMQP协议规定只有Channel(信道)才能执行AMQP的命令,AMQP的命令都是通过Channel(信道)送出去的。每条Channel(信道)都会被指派一个唯一ID;一个Connection(连接)能够包含多个Channel(信道)。因此需要建立Channel(信道),是由于TCP连接的创建和释放都是十分昂贵的,如果一个客户端每一个线程都需要与RabbitMQ服务器交互,且每一个线程都建立一个TCP连接,暂且不考虑TCP连接是否浪费,就连操作系统也没法承受创建如此多的TCP连接。RabbitMQ建议客户端线程之间不要共用Channel(信道),至少要保证共用Channel(信道)的线程发送消息必须是串行的,但是建议尽量共用Connection(连接)。

  5. Broker(节点)
       接收和分发消息的应用,简单来说就是RabbitMQService(消息队列服务器)实体。

  6. Virtual host(虚拟消息服务器)
       每个VirtualHost相当于一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通。

  7. RoutingKey(路由key)
       生产者将消息发给交换器的时候,指定一个RoutingKey, 用来指定这个消息的路由规则,它决定了消息流向哪里;当BindingKey和RoutingKey相匹配的时候,消息会被路由到对应的队列中。

  8. Exchange(交换机)
       消息到达 RabbitMQ服务器的第一站,生产者的消息不是直接投递到Queue(队列)中的,实际上是生产者将消息发送到Exchange(交换器),由Exchange(交换器)将消息路由到一个或多个Queue(队列)中(或者丢弃)。Exchange(交换机)不会存储消息。如果没有Queue(队列)绑定到Exchange,它会直接丢弃生产者发送过来的消息。这里有一个比较重要的概念:路由键(RoutingKey)。当消息发送到Exchange(交换器)的时候,Exchange(交换器)会将其转发到对应的Queue(队列)中,至于转发到哪个Queue(队列)取决于路由键。

  9. BindingKey(绑定key)
       将特定的Exchange与特定的Queue绑定起来,绑定的关系是BindingKey。Exchange和Queue的绑定可以是多对多的关系,每个发送给Exchange的消息都有一个叫作RoutingKey的关键字,Exchange要将该消息转发给特定的Queue,该Queue队列与Exchange的BindingKey必须与消息的RoutingKey相匹配。
       切记:“真实情况下参数名都是RoutingKey,代码中没有BindingKey这个参数的,为了区别消费者发送的key和消费者绑定key,我们才说RoutingKey和BindingKey”, 这是为了好区分而取的名字。

  10. Queue(队列)
       队列是RabbitMQ的内部对象,用于存储消息;每个消息都会被投入到一个或多个队列中。消息最终被送到这里等待消费者取走。在应用程序的权限范围内可以自由地创建、共享、使用和消费Queue(队列)。Queue(队列)提供有限制的先进先出保证,服务器会将某一个生产者发出的同等优先级消息按照它们进入队列的顺序传递给某一个消费者,Queue(队列)可以是持久的、临时的或者自动删除的。Queue(队列)可以保存在内存、硬盘或者两种介质的组合中。

  11. Consumer(消费者)
       用于接收消费消息。

1.2.4. 安装

  这里采用docker安装rabbitmq 步骤比较繁琐。就不一一展示了。没有docker环境的同学可以下载rabbitmq的windows安装包一键安装。

安装问题解决
1、如果打开的界面出现如下问题
在这里插入图片描述
解决方法:
1、进入 rabbitmq的容器 :docker exec -it 容器id /bin/bash
在这里插入图片描述
2、进入目录: cd /etc/rabbitmq/conf.d/
在这里插入图片描述
3、执行命令:echo management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf
在这里插入图片描述
4、退出当前容器,然后重启容器 ;退出当前容器快捷键:【ctrl+p+q】 重启容器命令: docker restart 容器id
在这里插入图片描述

1.2.5. RabbitMQ 核心部分

1.简单模式

1 “Hello World!”
The simplest thing that does something
一个生产者将消息发送到一个队列,一个消费者从该队列接收和处理消息,确保每个消息只被一个消费者接收。
在这里插入图片描述
案例说明:由生产者产生消息并发送到RabbitMQ队列中,然后消费者从RabbitMQ队列中取出消息。

生产者代码示例

package com.leo.one;

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

import java.nio.charset.StandardCharsets;

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {

    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_hello";

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

        //1、创建一个rabbitmq包下的连接工厂,并设置连接参数
        ConnectionFactory rabbitMqFactory = new ConnectionFactory();
        rabbitMqFactory.setHost("192.168.58.128");//设置连接rabbitMQ服务ip地址
        rabbitMqFactory.setPort(5672);//设置连接rabbitMQ服务的端口
        rabbitMqFactory.setUsername("admin");//设置连接rabbitMQ服务账号
        rabbitMqFactory.setPassword("admin");//设置连接rabbitMQ服务密码
        rabbitMqFactory.setVirtualHost("my_vhost");//设置连接rabbitMQ服务的虚拟机

        //2、使用工厂(rabbitMqFactory)创建一个连接通道。
        Connection rabbitMqConnection = rabbitMqFactory.newConnection();

        //3、使用通道创建一个信道
        Channel channel = rabbitMqConnection.createChannel();

        /**
         * 4、创建一个队列
         * 方法参数说明:
         *   String queue:队列的名称
         *   boolean durable: 设置是否持久化。为true则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
         *   boolean exclusive:设置是否排他。为true则设置队列为排他的。
         *                     如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,
         *                     并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接( Connection)可见的,
         *                     同个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;
         *                     "首次"是指如果个连接己经声明了排他队列,其他连接是不允许建立同名的排他队列的,
         *                     这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,
         *                     该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。
         *   boolean autoDelete:设置是否自动删除。为true则设置队列为自动删除。
         *                      自动删除的前提是:至少有一个消费者连接到这个队列,
         *                      之后所有与这个队列连接的消费者都断开时,才会自动删除。
         *                      不能把这个参数错误地理解为:"当连接到此队列的所有客户端断开时,
         *                      这个队列自动删除",因为生产者客户端创建这个队列,
         *                      或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。
         *   Map<String, Object> arguments: 设置队列的其他一些参数,如x-rnessage-ttl;
         *                                  x-expires;x-rnax-length;
         *                                  x-rnax-length-bytes;
         *                                  x-dead-letter-exchange;
         *                                  x-deadletter-routing-key;
         *                                  x-rnax-priority等。
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //发送到rabbitMq的信息
        String msg = "hello rabbitmq";
        //5、发送消息的方法
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
        System.out.println("发送消息成功!");

        //关闭通道
        channel.close();
        //关闭连接
        rabbitMqConnection.close();
    }
}

消费者代码示例

package com.leo.one;

import com.rabbitmq.client.*;

/**
 * 消费者
 */
public class Consumer {

    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_hello";

    public static void main(String[] args) throws Exception {
        //1、创建一个rabbitmq包下的连接工厂。
        ConnectionFactory rabbitMqFactory = new ConnectionFactory();
        rabbitMqFactory.setHost("192.168.58.128");//设置连接rabbitMQ服务ip地址
        rabbitMqFactory.setPort(5672);//设置连接rabbitMQ服务的端口
        rabbitMqFactory.setUsername("admin");//设置连接rabbitMQ服务账号
        rabbitMqFactory.setPassword("admin");//设置连接rabbitMQ服务密码
        rabbitMqFactory.setVirtualHost("my_vhost"); //设置连接rabbitMQ服务的虚拟机

        //2、使用工厂(rabbitMqFactory)创建一个连接通道。
        Connection rabbitMqConnection = rabbitMqFactory.newConnection();

        //3、使用通道创建一个信道
        Channel channel = rabbitMqConnection.createChannel();

        //4.1、接收的消息被正常接收需要回调的接口 (这里接收成功直接打印出消息)
        DeliverCallback deliverCallback = (consumerTag, delivery) -> System.out.println("接收到的消息是:" + new String(delivery.getBody()));
        //4.2、接收的消息被取消或者接收异常需要回调的接口
        CancelCallback cancelCallback = (consumerTag) -> System.out.println("消息接收失败!");
        //4、接收消息方法
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);

        //关闭通道
        channel.close();
        //关闭连接
        rabbitMqConnection.close();
    }
}

2.工作队列模式

2 Work queues
Distributing tasks among workers (the competing consumers pattern)
在工人之间分配任务(竞争消费者模式)

2.1、模式说明:

一个消息生产者,一个消息队列,多个消费者。同样也称为点对点模式。在这种模式中又可以分为两种模式,
轮询分发:一种是两个消费中平均消费队列中的消息。也就说无论两个消费者的消费能力如何,都会平均获取并消费消息。
公正分发(或不公平分发):另一种方式则是,能者多劳模式,处理消息能力强的消费者会获取更多的消息,这种模式似乎更符合一些。

在这里插入图片描述

2.2、案例实现步骤

1.创建生产者,在生产中创建一个队列。通过控制台输入的方式向队列中发送消息
2、创建两个消费者,两个消费者监听的队列都是生产者创建的队列
3、由于生产者和消费者都要通过连接工厂创建连接。所以把相同的代码抽取出来写成工具类。

2.3、代码如下
2.3.1、工具类
package com.leo.two;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Objects;

public class RabbitMQUtils {
    /**
     * 创建一个连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //1、创建一个rabbitmq包下的连接工厂。
        ConnectionFactory rabbitMqFactory = new ConnectionFactory();
        rabbitMqFactory.setHost("192.168.58.128");//设置连接rabbitMQ服务ip地址
        rabbitMqFactory.setPort(5672);//设置连接rabbitMQ服务的端口
        rabbitMqFactory.setUsername("admin");//设置连接rabbitMQ服务账号
        rabbitMqFactory.setPassword("admin");//设置连接rabbitMQ服务密码
        rabbitMqFactory.setVirtualHost("my_vhost"); //设置连接rabbitMQ服务的虚拟机

        //2、使用工厂(rabbitMqFactory)创建一个连接通道。
        Connection rabbitMqConnection = null;
        if (Objects.isNull(rabbitMqConnection)) {
            rabbitMqConnection = rabbitMqFactory.newConnection();
        }
        return rabbitMqConnection;
    }
}
2.3.2、生产者
package com.leo.two;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {

    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_hello";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.getConnection();

        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        /**
         * 4、创建一个队列
         * 方法参数说明:
         *   String queue:队列的名称
         *   boolean durable: 设置是否持久化。为true则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
         *   boolean exclusive:设置是否排他。为true则设置队列为排他的。
         *                     如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,
         *                     并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接( Connection)可见的,
         *                     同个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;
         *                     "首次"是指如果个连接己经声明了排他队列,其他连接是不允许建立同名的排他队列的,
         *                     这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,
         *                     该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。
         *   boolean autoDelete:设置是否自动删除。为true则设置队列为自动删除。
         *                      自动删除的前提是:至少有一个消费者连接到这个队列,
         *                      之后所有与这个队列连接的消费者都断开时,才会自动删除。
         *                      不能把这个参数错误地理解为:"当连接到此队列的所有客户端断开时,
         *                      这个队列自动删除",因为生产者客户端创建这个队列,
         *                      或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。
         *   Map<String, Object> arguments: 设置队列的其他一些参数,如x-rnessage-ttl;
         *                                  x-expires;x-rnax-length;
         *                                  x-rnax-length-bytes;
         *                                  x-dead-letter-exchange;
         *                                  x-deadletter-routing-key;
         *                                  x-rnax-priority等。
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //5、通过控制台输入消息并发送
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String msg = scanner.next();
            //6、发送消息的方法
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("发送消息成功!发送的消息为:" + msg);
        }
        //关闭通道
        // channel.close();
    }
}

运行结果
在这里插入图片描述

2.3.3、消费者1
package com.leo.two;
import com.rabbitmq.client.*;
/**
 * 消费者1
 */
public class Consumer1 {

    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_hello";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        //4.1、接收的消息被正常接收需要回调的接口 (这里接收成功直接打印出消息)
        DeliverCallback deliverCallback = (consumerTag, delivery) -> System.out.println("接收到的消息是:" + new String(delivery.getBody()));
        //4.2、接收的消息被取消或者接收异常需要回调的接口
        CancelCallback cancelCallback = (consumerTag) -> System.out.println("消息接收失败!");

        System.out.println("消费者1:等待接收消息...");
        //4、接收消息方法
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);

        //关闭通道
        //channel.close();
    }
}

运行结果
在这里插入图片描述

2.3.4、消费者2
package com.leo.two;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DeliverCallback;

/**
 * 消费者2
 */
public class Consumer2 {
    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_hello";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        //4.1、接收的消息被正常接收需要回调的接口 (这里接收成功直接打印出消息)
        DeliverCallback deliverCallback = (consumerTag, delivery) -> System.out.println("接收到的消息是:" + new String(delivery.getBody()));
        //4.2、接收的消息被取消或者接收异常需要回调的接口
        CancelCallback cancelCallback = (consumerTag) -> System.out.println("消息接收失败!");

        System.out.println("消费者2:等待接收消息...");
        //4、接收消息方法
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);

        //关闭通道
        //channel.close();

    }
}

运行结果
在这里插入图片描述

2.3.5、总结:

  从运行的结果可以看出。消费者1和2 消费的消息个数都是3个,消费消息的方式为轮询。

2.4、如何实现【不公平分发】。

什么是不公平分发:就是能者多劳。性能高的线程可以多处理消息。

生产者只负责发送消息。至于如何消费,生产者不管。所以代码修改方都在消费者这边。

1、将多个消费者的自动应答机制改为手动应答

channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);

在这里插入图片描述
2、在接收消息成功的回调方法中调用信道的手动应答方法,并在消费者2中应答方法前停留30秒

channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

basicAck()方法参数说明:
  参数1:应答的消息标签(类似于数据库表数据的id),
  参数2:表示是否批量应答;false代表只应答收到的那个消息;true应答所有传递过来的消息。

消费者1
在这里插入图片描述
消费者2
在这里插入图片描述

3、使用basicQos控制速率
  说明:默认情况下,RabbitMq收到消息后,就向消费者全部推送。但是如果rabbitmq队列里消息过多,且消息的数量超过了消费者处理能力, 就会导致客户端超负荷崩溃。此时我们可以通过 prefetchCount 限制每个消费者在收到下一个确认回执前一次可以最大接受多少条消息。即如果设置prefetchCount =1,RabbitMQ向这个消费者发送一个消息后,再这个消息的消费者对这个消息进行ack之前,RabbitMQ不会向这个消费者发送新的消息。所以在每个消费者中加上这行代码

 channel.basicQos(1);
 或者代码
 channel.basicQos(0,1,false);

参数说明:
void basicQos(int prefetchSize, int prefetchCount, boolean global)

  a、prefetchSize:可接收消息的大小的;
  b、prefetchCount:处理消息最大的数量。举个例子,如果输入1,那如果接收一个消息,但是没有应答,则客户端不会收到下一个消息,消息只会在队列中阻塞。如果输入3,那么可以最多有3个消息不应答,如果到达了3个,则发送端发给这个接收方得消息只会在队列中,而接收方不会有接收到消息的事件产生。总结说,就是在下一次发送应答消息前,客户端可以收到的消息最大数量。
  c、global:是不是针对整个Connection(连接)的,因为一个Connection可以有多个Channel(信道),如果是false则说明只是针对于这个Channel(信道)的。
在这里插入图片描述

执行结果:向mq中写入四条消息。消费者2智能消费1条(因为消费者2消费一条消息要30秒),消费者1额可以消费3条

发送四条消息
在这里插入图片描述

消费者1:消费3条消息
在这里插入图片描述

消费者2:消费1条消息
在这里插入图片描述


3.发布/订阅模式

3 Publish/Subscribe
Sending messages to many consumers at once
多个消费者订阅一个交换机(Exchange),生产者发送消息到交换机,交换机将消息广播到所有订阅者。
在这里插入图片描述


4.路由模式

4 Routing
Receiving messages selectively
在发布/订阅模式的基础上增加路由功能,通过路由器将消息发送到指定的队列,实现更灵活的消息路由和处理。
在这里插入图片描述


5.主题模式

5 Topics
Receiving messages based on a pattern (topics)
生产者发送带有主题标签的消息到交换机,交换机根据主题将消息路由到一个或多个队列,消费者可以根据自己的需求选择性地接收消息。
在这里插入图片描述


6.RPC模式

6 RPC
Request/reply pattern example
请求/应答模式的例子
在这里插入图片描述


1.2.6. 消息应答

1、什么是消息应答:

  消费者在接收到消息并且处理该消息之后,告诉 RabbitMQ 它已经收到消息,并对消息进行了处理了。RabbitMQ 可以根据收到的结果对消息进行处理。

2、消息应答分为两种

  1. 自动应答: 默认情况下,rabbitmq开启了消息的自动应答。此时,一旦rabbitmq将消息分发给了消费者,就会将消息从内存中删除。这种情况下,如果正在执行的消费者被“杀死”或“崩溃”,就会丢失正在处理的消息。这种模式需要在高吞吐量和数据传输安全性方面做权衡,所以不推荐。
   2. 手动应答: 消费者就收到消息后,处理完任务再进行代码手动应答。MQ得到应答后,再将消息从内存中删除。好处:当消费者消费消息时,消费者中途挂了,MQ没有得到应答,那么消息将会再次发送给消费者,或者再次发送给另一个消费者进行处理。

3、手动应答的方法

  1. basicAck(long deliveryTag, boolean multiple) :消息消费成功,用于肯定确认,【deliveryTag】表示当前被消费的消息编号。【multiple】表示是否批量应答

  2. basicNack(long deliveryTag, boolean multiple, boolean requeue): 用于否定确认;【deliveryTag】表示当前被消费的消息编号。【multiple】表示是否批量应答;【requeue】是否将该条消息重新返回队列,以便再次消费。

  3. basicReject(long deliveryTag, boolean requeue):用于拒绝消费该消息。【deliveryTag】表示当前被消费的消息编号。【requeue】是否将该条消息重新返回队列,以便再次消费。

4、使用步骤

a、创建一个消息生产者。
b、创建一个消费者。把消费者的自动应答改为false,在消息消费【成功的回调方法】中调用手动应答的方法。

生产者

package com.leo.three;
import com.leo.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
 * 生产者
 * 目的:发消息
 */
public class Producer {

    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_ack";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建一个队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //5、通过控制台输入消息并发送
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String msg = scanner.next();
            //6、发送消息的方法
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("发送消息成功!发送的消息为:" + msg);
        }
        //关闭通道
        // channel.close();
    }
}

消费者

package com.leo.three;
import com.leo.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DeliverCallback;

/**
 * 消费者1
 */
public class Consumer {

    //1、设置一个队列名
    private static final String QUEUE_NAME = "queue_ack";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        //4.1、接收的消息被正常接收需要回调的接口 (这里接收成功直接打印出消息)
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            System.out.println("接收到的消息是:" + new String(delivery.getBody()));
            //手动应答方法
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        };

        //4.2、接收的消息被取消或者接收异常需要回调的接口
        CancelCallback cancelCallback = (consumerTag) -> System.out.println("消息接收失败!");

        //4、接收消息方法
        boolean autoAck = false;//是否自动应答。
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);

        //关闭通道
        //channel.close();
    }
}

消费者重点代码
在这里插入图片描述

1.2.7. RbbitMQ持久化

1.2.7.1概念

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

1.2.7.2 队列持久化

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

    //4、创建一个队列
        boolean durable=true;//队列是否持久化
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

在这里插入图片描述
持久化的队列会有一个【D】开头的字样。
在这里插入图片描述

注意:
  如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。 错误图片来源于网络。
在这里插入图片描述

1.2.7.3消息持久化

  在发送消息的方法上添加这个如下参数值。即可将消息持久化

 channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes(StandardCharsets.UTF_8));

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

1.2.7.3 预取值

ack:消费者收到消息后,返回的一个应答回复。
  在RabbitMQ中,队列向消费者发送消息,如果没有设置Qos的值,那么队列中有多少消息就发送多少消息给消费者,完全不管消费者是否能够消费完,这样可能就会形成大量未ack消息在缓存区堆积,因为这些消息未收到消费者发送的ack,所以只能暂时存储在缓存区中,等待ack,然后删除对应消息。这样的话,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 channel.basicQos()方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量,一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在信道上有未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的RAM 消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。

void basicQos(int prefetchSize, int prefetchCount, boolean global)
方法参数说明:
  a、prefetchSize:可接收消息的大小的;
  b、prefetchCount:处理消息最大的数量。举个例子,如果输入1,那如果接收一个消息,但是没有应答,则客户端不会收到下一个消息,消息只会在队列中阻塞。如果输入3,那么可以最多有3个消息不应答,如果到达了3个,则发送端发给这个接收方得消息只会在队列中,而接收方不会有接收到消息的事件产生。总结说,就是在下一次发送应答消息前,客户端可以收到的消息最大数量。
  c、global:是不是针对整个Connection(连接)的,因为一个Connection可以有多个Channel(信道),如果是false则说明只是针对于这个Channel(信道)的。

“是不是很有印象,是的,就是上面的不公平分发的里面控制速率方法。前面设置的预取值就是1。也就是消费一个然后再推送一个。”

在这里插入图片描述

1.3. 发布确认

1.3.1、什么是发布确认

  就是消息生产者将消息发送到rabbitmq服务器。然后服务器返回一个确认收到的消息给生产者。表示这条消息rabbitmq服务器已经确认收到。

1.3.2、rabbitmq发布确认原理

confirm:确认。

  生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,rabbitmq服务器就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,rabbitmq服务器(broker )回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外rabbitmq服务器(broker )也可以设置basic.ack 的multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
  confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

1.3.3、单个确认发布

1.3.3.1说明

  这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirms(long timeout)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
  这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

1.3.3.2实现代码

//单个确认机制
    public static void individualConfirmation() throws Exception {
        //1、获取通道
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //设置一个对列名。
        String queueName = "individual_confirmation";
        //4、使用信息创建队列
        channel.queueDeclare(queueName, false, false, false, null);
        //5、开启发布确认
        channel.confirmSelect();
        //6、获取当前时间为开始时间
        long begin = System.currentTimeMillis();
        //7、发送1000条消息到rabbitmq中
        for (int i = 0; i < 1000; i++) {
            String message = i + "条消息";
            //8、推送消息
            channel.basicPublish("", queueName, null, message.getBytes(StandardCharsets.UTF_8));
            //9、接收返回确认信息;服务端返回 false 或规定超时时间内未返回,生产者可以继续发送消息。失败的先不管。
            boolean flag = channel.waitForConfirms();
            if (flag) {
                System.out.println("消息发送成功");
            }
        }
        //10、获取当前时间为结束时间,计算耗时时间。
        long end = System.currentTimeMillis();
        System.out.println("发布" + 1000 + "个单独确认消息,耗时" + (end - begin) + "ms");
    }

1.3.3.3主要代码:

  第5和第10步代码。

1.3.3.4结果:

在这里插入图片描述

1.3.4、批量确认发布

1.3.4.1、说明

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

1.3.4.1、实现代码

    //批量确认机制
    public static void batchConfirmation() throws Exception {
        //1、获取通道
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        String queueName = "individual_confirmation";//设置一个对列名。
        //4、使用信息创建队列
        channel.queueDeclare("queueName", false, false, false, null);
        //5、开启发布确认
        channel.confirmSelect();
        //6、获取当前时间为开始时间
        long begin = System.currentTimeMillis();
        //7、发送1000条消息到rabbitmq中
        int count = 0;
        for (int i = 0; i < 1000; i++) {
            String message = i + "条消息";
            //8、推送消息
            channel.basicPublish("", queueName, null, message.getBytes(StandardCharsets.UTF_8));
            count++;
            //9、每推送100条消息为一批进行一个确认。
            if (count == 100) {
                //10、接收返回确认信息;服务端返回 false 或规定超时时间内未返回,生产者可以继续发送消息。失败的先不管。
                boolean flag = channel.waitForConfirms();
                if (flag) {
                    System.out.println("消息发送成功");
                }
                count = 0;//归零。
            }
        }
        //11、如果最后小于100也确认一次。
        if (count > 0) {
            boolean flag = channel.waitForConfirms();
            if (flag) {
                System.out.println("消息发送成功");
            }
        }
        //12、获取当前时间为结束时间,计算耗时时间。
        long end = System.currentTimeMillis();
        System.out.println("发布" + 1000 + "个单独确认消息,耗时" + (end - begin) + "ms");
    }

1.3.4.2、主要代码

  第5、7、8、9、10、11步代码。

1.3.4.3、结果

在这里插入图片描述

1.3.5、异步确认发布

1.3.5.1、说明

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

1.3.5.2、实现是代码

    //异步确认机制
    public static void asynchronousConfirmation() throws Exception {
        //1、获取通道
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //设置一个对列名。
        String queueName = "asynchronous_Confirmation";
        //4、使用信息创建队列
        channel.queueDeclare(queueName, false, false, false, null);
        //5、开启发布确认
        channel.confirmSelect();

        //6.1、消息确认成功回调函数
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
            System.out.println("消息确认成功的消息:" + deliveryTag);
        };
        //6.2、消息确认失败回调函数
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            System.out.println("消息确认失败的消息:" + deliveryTag);
        };

        /**
         * 6、添加一个异步确认的监听器
         * 参数1.确认收到消息的回调
         * 参数2.未收到消息的回调
         */
        channel.addConfirmListener(ackCallback, nackCallback);

        //7、获取当前时间为开始时间
        long begin = System.currentTimeMillis();
        //8、推送1000条消息
        for (int i = 0; i < 1000; i++) {
            String message = i + "条消息";
            //9、推送消息
            channel.basicPublish("", queueName, null, message.getBytes(StandardCharsets.UTF_8));
        }
        //10、获取当前时间为结束时间,计算耗时时间。
        long end = System.currentTimeMillis();
        System.out.println("发布" + 1000 + "个【异步确认】消息,耗时" + (end - begin) + "ms");
    }

1.3.5.3、主要代码

  第5、6步代码。

1.3.5.4、结果

在这里插入图片描述

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

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

实现步骤:
1、将推送的数据存储一个多线程的Map中。
2、将推送并应答成功的消息从这个map中删除。
3、然后剩下的失败的该怎么处理就怎么处理。

实现代码

    //异步确认机制
    public static void asynchronousConfirmation() throws Exception {
        //1、获取通道
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //设置一个对列名。
        String queueName = "asynchronous_Confirmation";
        //4、使用信息创建队列
        channel.queueDeclare(queueName, false, false, false, null);
        //5、开启发布确认
        channel.confirmSelect();

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

        //7.1、消息确认成功回调函数
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
             System.out.println("消息确认成功的消息:" + deliveryTag);
            //如果是批量确认则清空这个map
            if (multiple) {
                //返回的是小于等于当前序列号的未确认消息 是一个 map
                ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag,true);
                //清除该部分未确认消息
                confirmed.clear();
            } else {
                //单个确认只清除当前序列号的消息
                outstandingConfirms.remove(deliveryTag);
            }
         };
        //7.2、消息确认失败回调函数
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            System.out.println("消息确认失败的消息:" + deliveryTag);
        };

        /**
         * 7、添加一个异步确认的监听器
         * 参数1.确认收到消息的回调
         * 参数2.未收到消息的回调
         */
        channel.addConfirmListener(ackCallback, nackCallback);

        //8、获取当前时间为开始时间
        long begin = System.currentTimeMillis();
        //9、推送1000条消息
        for (int i =0; i < 1000; i++) {
            String message = i + "条消息";
            //10、推送消息
            channel.basicPublish("", queueName, null, message.getBytes(StandardCharsets.UTF_8));
            //11、记录下发送过的消息。
            outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
        }
        //12、获取当前时间为结束时间,计算耗时时间。
        long end = System.currentTimeMillis();
        System.out.println("发布" + 1000 + "个【异步确认】消息,耗时" + (end - begin) + "ms");
        Thread.sleep(10000);
    }

主要代码
  第5、6、7、11步代码。

1.4、Exchanges(交换机)

1.4.1、Exchanges 概念

  RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递到了哪些队列中。相反,生产者只能将消息发送给交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。

1.4.2、Exchanges 的类型

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

1.4.3、无名交换机

  在本文章的前面部分我们对 交换机(exchange) 一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。
在这里插入图片描述
  第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话。

1.4.4、临时队列

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

创建临时队列的方式如下:
  String queueName = channel.queueDeclare().getQueue();
创建出来之后长成这样:
在这里插入图片描述

1.4.5、绑定(binding)

  什么是 bingding 呢,binding 其实是 exchange (交换机)和 queue(队列) 之间的桥梁,它告诉我们 exchange(交换机) 和哪个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定。
在这里插入图片描述

1.4.6、交换机类型之 Fanout(扇出/广播模式)

  fanout类型的Exchange(交换机)路由规则非常简单,只要与该交换机绑定的队列,该交换机会把消息路由到与它绑定的每个队列中。

1.4.6.1、实现步骤

  1. 创建1个生产者,在生产者中创建一个交换机,设置交换机的名称和类型(设置为:fanout 类型),然后发送消息,发送消息方法指定创建好的交换机。
  2. 创建两个消费者,消费者中将交换和队列进行绑定,队列名可以使用信道调用方法随机生成,然后在接收消息的方法中打印接收到的消息。
  3. 结果:两个消费者都能同时接收到生产者发送过来的消息。

1.4.6.2、具体代码

生产者代码

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {

    //1、取一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_FANOUT";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();

        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        //4、创建一个交换机,设置交换机的名称和类型
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");

        //5、通过控制台输入消息并发送
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String msg = scanner.next();
            //6、发送消息的方法
            channel.basicPublish(EXCHANGE_NAME,"", null, msg.getBytes(StandardCharsets.UTF_8));
            System.out.println("发送消息成功!发送的消息为:" + msg);
        }
        //关闭通道
        // channel.close();
    }
}

消费者代码1

/**
 * 消费者1
 */
public class Consumer1 {
    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_FANOUT";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //创建交换机(生产者和消费者都加上创建交换机的代码可以避免出现交换机不存在而报错。)
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        //4、创建一个随机的队列名
        String queue_name = channel.queueDeclare().getQueue();
        //5、将队列和指定好的交换机进行绑定。
        channel.queueBind(queue_name, EXCHANGE_NAME, "");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者1接收到的消息打印在控制台: " + message);
        };
        //6、接收消息方法
        channel.basicConsume(queue_name, false, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

消费者代码2

/**
 * 消费者2
 */
public class Consumer2 {
    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_FANOUT";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //创建交换机(生产者和消费者都加上创建交换机的代码可以避免出现交换机不存在而报错。)
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
        //4、创建一个随机的队列名
        String queue_name = channel.queueDeclare().getQueue();
        //5、将队列和指定好的交换机进行绑定。
        channel.queueBind(queue_name, EXCHANGE_NAME, "");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者2接收到的消息打印在控制台: " + message);
        };
        //6、接收消息方法
        channel.basicConsume(queue_name, false, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

1.4.7、交换机类型之 Direct(路由模式)

  生产者指定一个路由key(routing_key),然后交换机根据路由key将信息转发到与之相匹配的队列中。

1.4.7.1、案例需求

  1. 生产者需要发送三种类型的日志由不同的消费者来进行消费,日志类型分别为:正常日志、警告日志、错误日志。
  2. 正常消费者消费正常日志,警告消费者消费警告日志,错误消费者消费错日志。

1.4.7.2、实现步骤

  1. 在消费者中创建3单个类型的路由key,创建一个交换机,并设置交换机类型为direct,通过交换机发送消息到rabbmq服务器。消息分别为正常日志消息,警告日志消息,错误日志消息。并指定路由key分别为正常日志路由key、警告日志路由key、错误日志路由key;
  2. 在消费者中绑定好交换机和路由key。

生产者代码:重点代码在第6点

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {
    //1、取一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_DIRECT";

    //自定义三个【路由key】
    private static final String ROUTING_KEY_NORMAL = "routing_key_normal";//正常日志路由key
    private static final String ROUTING_KEY_WARNING = "routing_key_warning";//警告日志路由key
    private static final String ROUTING_KEY_ERROR = "routing_key_error";//错误日志路由key

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建一个交换机,设置交换机的名称和类型
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        //6、发送消息的方法
        //6.1、将日志消息发送到正常消费者
        String normal_msg = "正常日志消息";
        channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY_NORMAL, null, normal_msg.getBytes(StandardCharsets.UTF_8));
        System.out.println("发送消息成功!发送的消息为:" + normal_msg);

        //6.2、将日志消息发送到警告消费者
        String warning_msg = "警告日志消息";
        channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY_WARNING, null, warning_msg.getBytes(StandardCharsets.UTF_8));
        System.out.println("发送消息成功!发送的消息为:" + warning_msg);

        //6.3、将日志消息发送到错误消费者
        String error_msg = "错误日志消息";
        channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY_ERROR, null, error_msg.getBytes(StandardCharsets.UTF_8));
        System.out.println("发送消息成功!发送的消息为:" + error_msg);
        //关闭通道
        // channel.close();
    }
}

发送结果
在这里插入图片描述


消费者1的代码 重点代码在第5点

/**
 * 消费者1 接收正常日志
 */
public class Consumer1 {

    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_DIRECT";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建一个随机的队列名
        String queue_name = channel.queueDeclare().getQueue();
        //5、将队列和指定好的交换机进行绑定。
        String ROUTING_KEY_NORMAL = "routing_key_normal";//正常日志路由key
        channel.queueBind(queue_name, EXCHANGE_NAME, ROUTING_KEY_NORMAL);

        //6.1打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者1接收到正常日志打印在控制台: " + message);
        };

        //6、接收消息方法
        channel.basicConsume(queue_name, false, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

消费者1接收结果
在这里插入图片描述


消费者2的代码 重点代码在第5点

/**
 * 消费者2 接收警告日志
 */
public class Consumer2 {

    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_DIRECT";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建一个随机的队列名
        String queue_name = channel.queueDeclare().getQueue();
        //5、将队列和指定好的交换机进行绑定,并指路由Key
        String ROUTING_KEY_WARNING = "routing_key_warning";//警告日志路由key
        channel.queueBind(queue_name, EXCHANGE_NAME, ROUTING_KEY_WARNING);

        //6.1打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者2接收到警告日志打印在控制台: " + message);
        };

        //6、接收消息方法
        channel.basicConsume(queue_name, false, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

消费者2接收结果
在这里插入图片描述


消费者3的代码 重点代码在第5点

/**
 * 消费者3 接收错误日志
 */
public class Consumer3 {

    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_DIRECT";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建一个随机的队列名
        String queue_name = channel.queueDeclare().getQueue();
        //5、将队列和指定好的交换机进行绑定,并指路由Key
        String ROUTING_KEY_ERROR = "routing_key_error";//错误日志路由key
        channel.queueBind(queue_name, EXCHANGE_NAME, ROUTING_KEY_ERROR);

        //6.1打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者3接收到错误日志打印在控制台: " + message);
        };

        //6、接收消息方法
        channel.basicConsume(queue_name, false, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

消费者3接收结果
在这里插入图片描述

1.4.8、交换机类型之 Topic(主题模式)

  Topic(主题模式)类型与Direct(路由模式)类型不同,Direct必须是生产者发布消息指定的routingKey和消费者在队列绑定时指定的routingKey(或叫绑定key)完全相等时才能匹配到队列上。与Direct不同的是,Topic可以进行模糊匹配,可以使用星号 * 和 # 这两个通配符来进行模糊匹配,其中 * 可以代替一个单词; topic类型的交换机的消息 routing_key 不能随意写,必须满足一定的要求,它必须是一个单
词获取多个单词组成,以点号分隔开。这些单词可以是任意单词,但是一般都与消息的某些特性相关。一些合法的选择键的例子:“quick.orange.rabbit”,你可以定义任何长度数量的标识符,上限为255个字节。 # 井号可以替代零个或更多的单词,只要能模糊匹配上就能将消息映射到队列中。当一个队列的绑定键为 # 的时候,这个队列将会无视消息的路由键,接收所有的消息。

1.4.8.1、 匹配案例

1、下图绑定关系如下
在这里插入图片描述
2、解释绑定的关系
Q1–>绑定的是中间带 orange 带 3 个单词的字符串(* .orange.* )
Q2–>绑定的是最后一个单词是 rabbit 的 3 个单词(* .* .rabbit)
   第一个单词是 lazy 的多个单词(lazy.#)

3、 创建几个routingKey来看匹配结果,上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的。

quick.orange.rabbit ------>被队列 Q1、Q2 接收到
lazy.orange.elephant ------>被队列 Q1、Q2 接收到
quick.orange.fox------> 被队列 Q1 接收到
lazy.brown.fox------> 被队列 Q2 接收到
lazy.pink.rabbit ------>虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox ------>不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit ------>是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit ------>是四个单词但匹配 Q2

4、当队列绑定关系是下列这种情况时需要引起注意
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout(扇出) 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct (路由)了

1.4.8.2、 实战

实现上面的匹配案例
消费者1

/**
 * 消费者1
 */
public class Consumer1 {

    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_TOPIC";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建交换机(生产者和消费者都加上创建交换机的代码可以避免出现交换机不存在而报错。)
        channel.exchangeDeclare(EXCHANGE_NAME,"topic");
        //5、创建队列
        String queueName ="Q1";
        channel.queueDeclare(queueName, false, false, false, null);
        //5.1、队列与交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME,"*.orange.*" );
        System.out.println("等待接收Q1的消息........... ");

        //6.1、打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String routingKey = delivery.getEnvelope().getRoutingKey();
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者1接收到的消息:" + message+";routingKey是:"+routingKey);
        };
        //6、接收消息方法
        channel.basicConsume(queueName, true, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

消费者2

/**
 * 消费者2
 */
public class Consumer2 {

    //1、指定一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_TOPIC";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();
        //4、创建交换机(生产者和消费者都加上创建交换机的代码可以避免出现交换机不存在而报错。)
        channel.exchangeDeclare(EXCHANGE_NAME,"topic");
        //5、创建队列
        String queueName ="Q2";
        channel.queueDeclare(queueName, false, false, false, null);
        //5.1、队列与交换机绑定
        channel.queueBind(queueName, EXCHANGE_NAME,"*.*.rabbit" );
        channel.queueBind(queueName, EXCHANGE_NAME,"lazy.#" );
        System.out.println("等待接收Q1的消息........... ");

        //6.1打印消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String routingKey = delivery.getEnvelope().getRoutingKey();
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("消费者2接收到的消息:" + message+";routingKey是:"+routingKey);
        };
        //6、接收消息方法
        channel.basicConsume(queueName, true, deliverCallback, (consumerTag, delivery) -> {});

        //关闭通道
        //channel.close();
    }
}

生产者

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {
    //1、取一个交换机名
    private static final String EXCHANGE_NAME = "EXCHANGE_TOPIC";

    public static void main(String[] args) throws Exception {
        //2、获取连接
        Connection connection = RabbitMQUtils.getConnection();

        //3、使用通道创建一个信道
        Channel channel = connection.createChannel();

        //4、创建交换机(生产者和消费者都加上创建交换机的代码可以避免出现交换机不存在而报错。)
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        //5、创建routingKey,并根据routingKey依次发送消息
        Map<String, String> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
        bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到");
        bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到");
        bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到");
        bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次");
        bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
        bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
        bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");

        for (Map.Entry<String, String> 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);
        }
        
        //关闭通道
        // channel.close();
    }
}

结果:

生产者运行结果
在这里插入图片描述

消费者1接收结果
在这里插入图片描述
消费者2接收结果
在这里插入图片描述

1.5、死信队列

1.5.1、什么是死信队列

   用于存放死信消息的队列。

1.5.2、什么是死信消息

  1. 消息被拒绝消费并且不能重新回到队列(basic.reject / basic.nack,并且requeue = false)的消息;
  2. 队列溢出的消息(交换机推送过来的消息,队列长度不足,无法存放的消息);
  3. 过期的消息(超过了存活时间的消息);
  4. 如上3点也可以理解为无法消费的消息。

1.5.3、流程图

在这里插入图片描述

解释:正常情况下,消费者正常消费消息,如果消息无法消费消息则正常队列会将消息转发到与之绑定的死信交换机,由死信交换机发送到死信队列,由死信消费者消费消息。

1.5.4、实战

需求1:演示过期消息进入死信队列
   生产者发送10条消息,设置消息过期时间为10秒,10秒后消息有没有从正常队列推送到死信队列。

实现步骤:

  1. 创建正常交换机和死信交换机
  2. 创建正常队列和死信队列
  3. 正常交换机和正常队列绑定,死信交换机和死信队列绑定
  4. 正常队列设置其接收死信消息的死信交换机

生产者代码如下

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        /**
         * 1、创建正常交换机和死信交换机
         * 2、创建正常队列和死信队列,在正常队列中绑定死信交换机
         */

        //1.1、创建正常交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        //1.2、创建死信交换机
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //2.1、创建死信队列,绑定死信交换机
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "deadKey");

        //2.2、创建正常队列,绑定正常队列
        String normalQueue = "normal-queue";
        //2.2.1、正常队列设置它的死信队列信息
        Map<String, Object> params = new HashMap<>();
        //params.put("x-message-ttl", 10000);//设置队列中消息的存活时间 单位:毫秒
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);//正常队列设置其接收死信消息的死信交换机,参数key是固定值
        params.put("x-dead-letter-routing-key", "deadKey");// 设置死信交换机的routing-key,参数key是固定值
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "normalKey");

        //3、生产者发送消息
        //设置消息的TTL(time to live 存活时间)时间 为10秒
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        for (int i = 1; i < 11; i++) {
            String message = "info--》" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "normalKey", properties, message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }
    }
}

运行结果,10秒后消息消息从正常队列推送到了死信队列
在这里插入图片描述

需求2:演示队列消息溢出,多余消息进入死信队列
修改上面的代码。去掉消息过期时间,添加设置队列长度的参数。

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        /**
         * 1、创建正常交换机和死信交换机
         * 2、创建正常队列和死信队列,在正常队列中绑定死信交换机
         */

        //1.1、创建正常交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        //1.2、创建死信交换机
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //2.1、创建死信队列,绑定死信交换机
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "deadKey");

        //2.2、创建正常队列,绑定正常队列
        String normalQueue = "normal-queue";
        //2.2.1、正常队列设置它的死信队列信息
        Map<String, Object> params = new HashMap<>();
        //params.put("x-message-ttl", 10000);//设置队列中消息的存活时间 单位:毫秒
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);//正常队列设置其接收死信消息的死信交换机,参数key是固定值
        params.put("x-dead-letter-routing-key", "deadKey");// 设置死信交换机的routing-key,参数key是固定值
        params.put("x-max-length",6);//设置队列的消息存储长度。设置为等存储6条消息;
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "normalKey");

        //3、生产者发送消息
        //设置消息的TTL(time to live 存活时间)时间 为10秒
        //AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        for (int i = 1; i < 11; i++) {
            String message = "info--》" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "normalKey", null, message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }
    }
}

关键代码截图
在这里插入图片描述
注意:修改了队列的参数值。记得将队列删除掉然后再运行代码,不然会报错队列已存在错误。

运行结果:正常队列中有6条消息,死信队列中有4条。
在这里插入图片描述

需求3:消息消费被拒
步骤:

  1. 生产者去掉队列长度限制

生产者代码

/**
 * 生产者
 * 目的:发消息
 */
public class Producer {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        /**
         * 1、创建正常交换机和死信交换机
         * 2、创建正常队列和死信队列,在正常队列中绑定死信交换机
         */

        //1.1、创建正常交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        //1.2、创建死信交换机
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        //2.1、创建死信队列,绑定死信交换机
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "deadKey");

        //2.2、创建正常队列,绑定正常队列
        String normalQueue = "normal-queue";
        //2.2.1、正常队列设置它的死信队列信息
        Map<String, Object> params = new HashMap<>();
        //params.put("x-message-ttl", 10000);//设置队列中消息的存活时间 单位:毫秒
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);//正常队列设置其接收死信消息的死信交换机,参数key是固定值
        params.put("x-dead-letter-routing-key", "deadKey");// 设置死信交换机的routing-key,参数key是固定值
      //  params.put("x-max-length",6);//设置队列的消息存储长度。设置为等存储6条消息;
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "normalKey");

        //3、生产者发送消息
        //设置消息的TTL(time to live 存活时间)时间 为10秒
        //AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        for (int i = 1; i < 11; i++) {
            String message = "info--》" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "normalKey", null, message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }
    }
}

关键代码截图
在这里插入图片描述

  1. 新增一个正常消费者,将其中某一条消息拒绝消费,并设置为不重新退回队列,记得将是否自动应答改为false。

消费者代码

/**
 * 消费者1
 */
public class Consumer1 {

    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] args) throws Exception {
        //获取连接
        Connection connection = RabbitMQUtils.getConnection();
        //使用通道创建一个信道
        Channel channel = connection.createChannel();

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

        //打印接收的消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            if(message.equals("info--》5")){
                System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
                //requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
                channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
            }else {
                System.out.println("Consumer01 接收到消息"+message);
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);//手动应答
            }
        };
        String normalQueue = "normal-queue";//队列名称
        boolean autoAck=false;//是否自动应答
        channel.basicConsume(normalQueue, autoAck, deliverCallback, (CancelCallback) null);

        //关闭通道
        //channel.close();
    }
}

关键代码截图
在这里插入图片描述
运行结果:死信队列中只有 info–》5 这一条消息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.6、延迟队列

1.6.1、什么是延迟队列

   即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

例如

   下单后,30分中未支付,订单取消,回滚库存。

实现流程图
在这里插入图片描述

不过遗憾的是,RabbitMq中并未提供延迟队列的功能。

但是可以使用:TTL+死信队列组合实现延迟队列的效果。

实现流程图:说白了就是跟上面是【 1.5章节】的死信队列中【1.5.4实战】 【需求1 】的实现方法一摸一样。只是没有正常消费者,只有死信消费者。
在这里插入图片描述

实现原理:

   消息发送到普通交换机,普通交换机将消息推送到普通队列,当普通队列中的消息过期时,普通队列将消息发送给延迟交换机(或死信交换机),由延迟交换机(或死信交换机)转发给延迟队列(或死信队列),最后消费者消费消息。

实现步骤:

这里使用SpringBoot实现该需求
需要导入的依赖如下

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

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

<!-- rabbitmq依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

application.yml

server:
  port: 8080

spring:
  rabbitmq:
    host: 192.168.58.128
    port: 5672
    username: admin
    password: admin
    virtual-host: my_vhost

在这里插入图片描述

  1. 创建一个配置类;
  2. 声明普通交换机X,声明延迟交换机Y;
  3. 声明普通队列QA,将队列QA绑定交换机X,绑定的routingKey为XA;并延迟交换机为Y,绑定的routingKey为YD;设置队列消息过期时间为10秒;
  4. 声明普通队列QB,将队列QB绑定交换机X,绑定的routingKey为XB;并延迟交换机为Y,绑定的routingKey为YD;设置队列消息过期时间为40秒;
  5. 声明延迟队列QD,并绑定它的交换机为Y,绑定的routingKey为YD。
  6. 创建一个请求接口充当消息生产者发送消息
  7. 创建消费者消费消息。

代码
配置类代码

package com.leo.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * 配置类用于创建交换机、队列、以及绑定的关系。
 */
@Configuration
public class TtlQueueConfig {

    //普通交换机X
    public static final String EX = "X";
    //延迟交换机Y
    public static final String EY = "Y";

    //普通队列QA和QB
    public static final String QA = "QA";
    public static final String QB = "QB";
    //延迟队列QD
    public static final String QD = "QD";

    // 1、声明交换机 X
    @Bean
    public DirectExchange ex() {
        return new DirectExchange(EX);
    }

    // 2、声明延迟交换机 Y
    @Bean
    public DirectExchange ey() {
        return new DirectExchange(EY);
    }

    //3、声明队列 QA ,绑定延迟交换机;ttl 为 10s 并绑定到对应的
    @Bean
    public Queue qa() {
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的延迟交换机
        args.put("x-dead-letter-exchange", EY);
        //声明延迟交换机路由key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列中消息的 TTL(过期时间)
        args.put("x-message-ttl", 10000);
        return QueueBuilder.durable(QA).withArguments(args).build();
    }

    // 4、队列 QA 绑定交换机 X  routingKey为XA
    @Bean
    public Binding qa_binding_ex() {
        return BindingBuilder.bind(this.qa()).to(this.ex()).with("XA");
    }

    //5、声明队列 QB ,绑定延迟交换机;ttl 为 40s 并绑定到对应的
    @Bean
    public Queue qb() {
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的延迟交换机
        args.put("x-dead-letter-exchange", EY);
        //声明延迟交换机路由key
        args.put("x-dead-letter-routing-key", "YD");
        //声明队列中消息的 TTL(过期时间)
        args.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QB).withArguments(args).build();
    }

    //6、声明队列 QB 绑定 X 交换机
    @Bean
    public Binding qb_binding_ex() {
        return BindingBuilder.bind(this.qb()).to(this.ex()).with("XB");
    }

    //7、声明延迟队列 QD
    @Bean
    public Queue qd() {
        return new Queue(QD);
    }

    //8、声明死信队列 QD 绑定关系
    @Bean
    public Binding qd_binding_ey() {
        return BindingBuilder.bind(this.qd()).to(this.ey()).with("YD");
    }

}

消息生产者代码

package com.leo.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

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

    @Autowired
    private RabbitTemplate rabbitTemplate;

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

}

消费者代码

package com.leo.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    /**
     * 消费消息
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = "QD")
    public void receiveD(Message message)  {
        String msg = new String(message.getBody());
        log.info("当前时间:{},消费者收到死信队列信息{}", new Date().toString(), msg);
    }
}

然后再接口请求:http://localhost:8080/ttl/sendMsg/嘻嘻嘻

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

1.6.1、优化延迟队列

实现原理: 将消息的过期时间不在创建队列时创建。而是在消息发送时创建
实现步骤:

  1. 在上面的配置类中新增QC队列,并绑定X交换机,并设置延迟交换机。
  2. 在生产者生产消息时创建定义消息的过期时间。
    代码:
    配置类新增代码
    //1、新增普通队列QC
    public static final String QC = "QC";
    
    //2、声明队列 QC ,绑定延迟交换机
    @Bean
    public Queue qc() {
        Map<String, Object> args = new HashMap<>(2);
        //声明当前队列绑定的延迟交换机
        args.put("x-dead-letter-exchange", EY);
        //声明延迟交换机路由key
        args.put("x-dead-letter-routing-key", "YD");
        return QueueBuilder.durable(QC).withArguments(args).build();
    }

    //3、声明队列 QC 绑定 X 交换机
    @Bean
    public Binding qc_binding_ex() {
        return BindingBuilder.bind(this.qc()).to(this.ex()).with("XC");
    }

生产者接口代码,新增一个接口

    /**
     * 在发送消息时,设置效果过期时间。
     * @param message 消息内容
     * @param ttlTime 过期时间 单位毫秒
     */

    @GetMapping("sendExpirationMsg/{message}/{ttlTime}")
    public void sendMsg(@PathVariable String message, @PathVariable String ttlTime) {
        rabbitTemplate.convertAndSend("X", "XC", message, messagePostProcessor -> {
            messagePostProcessor.getMessageProperties().setExpiration(ttlTime);
            return messagePostProcessor;
        });
        log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列QC:{}", new Date(), ttlTime, message);
    }

执行请求
http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000
http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000

执行结果:
在这里插入图片描述

发现问题: 为什么收到消息的结果都是20000毫秒?延迟2000毫秒的消息也要20000毫秒后才能消费???

原因:
   由于队列的先进先出原则, RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到延迟队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

如何解决: 安装延时队列插件

1.6.2、Rabbitmq 插件实现延迟队列

解决上文延迟时间短的消息无法优先消费。

1.6.2.1、插件安装步骤
  1. 下载插件地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
    根据rabbitmq的版本选择下载,这里以RabbitMQ 3.9.11 为例

在这里插入图片描述
2. 将下载文件传到linux中(使用windoc与linux建立传输文件的桥梁来操作。这里不过多描述):
在这里插入图片描述

  1. 将文件拷贝到 rabbitmq的容器中
    将文件拷贝到rabbitmq容器的plugins目前下,这个目录是专门放插件的地方。如下图
    在这里插入图片描述

  2. 拷贝命令,在宿主机上操作,不是容器中:docker cp rabbitmq_delayed_message_exchange-3.9.0.ez ae9d690b9381:/plugins
    ae9d690b9381是容器的id
    在这里插入图片描述

  3. 去容器中查看是否拷贝成功
    在这里插入图片描述

  4. 在rabbitqm容器中激活插件
    命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
    注意:激活插件这里不可以带版本号,否则包插件不存在的错误。
    在这里插入图片描述

  5. 如下图,激活成功。
    在这里插入图片描述

  6. 退出容器并重启容器:docker restart ae9d690b9381
    在这里插入图片描述在这里插入图片描述

  7. 在页面,如果交换新增了一个类型:x-delayed-message 如下效果,表示插件激活成功。
    在这里插入图片描述

1.6.2.2、实现原理

   在上面的添加的插件中,我们新增一个新的交换机类型x-delayed-message,这个类型的交换机支持消息延迟发送,也就是说可以等消息在交换机中过期了然后再推送到队列中去给消费者消费,所以也就不存在普通队列转发消息到延迟交换机这一步了。
所以原理图如下:
在这里插入图片描述

新建配置配置类代码

package com.leo.config;

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

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

@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 = "delayed.routingKey";

    //1、声明自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机与队列的的类型routingKey匹配方式,这个是直接匹配,就是routingKey完全相等才将消息发送到队列。
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
    }

    //2、声明队列
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    //3、添加队列和交换机的绑定关系
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") CustomExchange delayedExchange) {
        return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

消息生产者,新增一个接口

    /**
     * @param message   消息内容
     * @param delayTime 过期时间 单位毫秒
     */
    @GetMapping("/sendDelayMsg/{message}/{delayTime}")
    public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
        rabbitTemplate.convertAndSend("delayed.exchange", "delayed.routingKey", message,
                messagePostProcessor -> {
                    messagePostProcessor.getMessageProperties().setDelay(delayTime);
                    return messagePostProcessor;
                });
        log.info("当前时间:{},发送一条延迟{}毫秒的信息给队列delayed.queue:{}", new Date(), delayTime, message);
    }

消费者代码

    /**
     * 消费消息
     *
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = "delayed.queue")
    public void receiveDelayedQueue(Message message) {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
    }

执行结果:正常。
在这里插入图片描述

1.6.3、总结

   延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。

1.7、发布确认(高级)

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

1.7.1、发布确认原理

   生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了。如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
   confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

1.7.2、实现步骤

代码架构图
在这里插入图片描述

1.7.2.1、开启触发回调机制

spring.rabbitmq.publisher-confirm-type=correlated
在这里插入图片描述

  • none:禁用发布确认模式,是默认值。
  • correlated:发布消息成功到交换器后会触发回调方法。
  • simple:经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
    waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker(rabbitmq服务器)。

1.7.2.2、添加配置类

package com.leo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 发布确认配置类
 */
@Configuration
public class ConfirmConfig {
    //声明交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return new DirectExchange("confirm.exchange");
    }

    // 声明队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable("confirm.queue").build();
    }

    // 声明队列绑定交换机关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }
}

1.7.2.3、实现交换机接收消息应答回调接口

package com.leo.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * 回调接口
 */
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
    /**
     * 交换机接收消息回调方法
     * @param correlationData
     * @param ack
     * @param cause
     */
    @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);
        }
    }
}

1.7.2.4、生产者接口

package com.leo.controller;

import com.leo.callback.MyCallBack;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;


@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    //1、获取回调接口实现类对象
    @Autowired
    private MyCallBack myCallBack;

    //2、依赖注入 rabbitTemplate 之后再设置它的回调对象
    @PostConstruct
    public void init() {
        //设置回调对象
        rabbitTemplate.setConfirmCallback(myCallBack);
    }

    /**
     * 发送消息
     *
     * @param message
     */
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {

        log.info("发送消息内容:{}", message);

        //4、正常填写正确的交换机名,演示交换机正常接收消息而回调的结果。
        CorrelationData correlationData1 = new CorrelationData("1"); //设置消息的唯一id 1
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1);

        /*******************************************************************************************************************/

        //5、故意写错交换机名(交换机名后面多拼接一截字符串),演示交换机无法收到消息而回调的结果。
        CorrelationData correlationData2 = new CorrelationData("2"); //设置消息的唯一id 2
        routingKey = "key2";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME + "-leo", routingKey, message + routingKey, correlationData2);
    }
}

1.7.2.5、消费者代码

package com.leo.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ConfirmConsumer {
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    /**
     * 消费消息
     *
     * @param message
     */
    @RabbitListener(queues = CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message) {
        String msg = new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}", msg);
    }
}

1.7.2.6、请求接口

http://localhost:8080/confirm/sendMessage/测试确认发布

结果 :消息id1 交换机正常收到。小id2 由于交换机错误,所以回调接口打印报错信息。说交换机错误
在这里插入图片描述

1.7.3、回退消息

   在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由(比如交换机发送消息到队列失败),那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

1.7.3.1、 实现步骤

1、开启叫交换机发送消息到队列失败的回调机制
spring:
  rabbitmq:
    publisher-returns: true

在这里插入图片描述

2、新增实现接口RabbitTemplate.ReturnsCallback 并重写实现方法
package com.leo.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MyReturnsCallback implements RabbitTemplate.ReturnsCallback {
    /**
     * 推送消息到队列失败后的的回调接口实现类方法
     *
     * @param returnedMessage
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        System.out.println("____________________________________");
        System.out.println("发送的消息内容:" + new String(returnedMessage.getMessage().getBody()));
        System.out.println("发送的交换机:" + returnedMessage.getExchange());
        System.out.println("应答编号:" + returnedMessage.getReplyCode());
        System.out.println("RoutingKey:" + returnedMessage.getRoutingKey());
        System.out.println("失败原因:" + returnedMessage.getReplyText());
        System.out.println("____________________________________");

    }
}
3、生产者新添加的代码

在这里插入图片描述
在这里插入图片描述
生产者完整代码

package com.leo.controller;

import com.leo.callback.MyCallBack;
import com.leo.callback.MyReturnsCallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;


@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    //1、获取回调接口实现类对象
    @Autowired
    private MyCallBack myCallBack;

    @Autowired
    private MyReturnsCallback myReturnsCallback;

    //2、依赖注入 rabbitTemplate 之后再设置它的回调对象
    @PostConstruct
    public void init() {
        //设置交换机接收消息回调对象
        rabbitTemplate.setConfirmCallback(myCallBack);
        //设置队列接收消息失败后的回调对象信息
        rabbitTemplate.setReturnsCallback(myReturnsCallback);
    }

    /**
     * 发送消息
     *
     * @param message
     */
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {

        log.info("发送消息内容:{}", message);

        //4、正常填写正确的交换机名,演示交换机正常接收消息而回调的结果。
        CorrelationData correlationData1 = new CorrelationData("1"); //设置消息的唯一id 1
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1);

        /**********************************************/

        //5、故意写错交换机名(交换机名后面多拼接一截字符串),演示交换机无法收到消息而回调的结果。
        CorrelationData correlationData2 = new CorrelationData("2"); //设置消息的唯一id 2
        routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME+"-leo", routingKey, message + routingKey, correlationData2);

        /**********************************************/

        //6、故意写一个不存在的routingKey,演示交换机发送消息到队列失败。
        CorrelationData correlationData3 = new CorrelationData("3"); //设置消息的唯一id 2
        routingKey = "key3";//一个不存在routingKey
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData3);
    }
}

4、执行结果 :失败原因是没有这routingKey

在这里插入图片描述

1.7.4、备份交换机

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

1.7.4.1、实现方式

代码构件图
在这里插入图片描述
1、在配置类中新增一个备份交换机 backup.exchange,新增一个备份队列 backup.queue、一个警告队列warning.queue,新增一个警告消费者。

2、备份队列和警告队列绑定备份交换机
3、正常交换机添加备份交换机
4、在生产者端估计写错交换机的 routingKey
5、编写警告消费者,看看警告消费者是否可以消费消息。

实现代码

配置类代码

package com.leo.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 发布确认配置类
 */
@Configuration
public class ConfirmConfig {

    //声明正常交换机,6、设置正常交换机的备份交换机。
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        //设置该交换机的备份交换机
        return ExchangeBuilder
                .directExchange("confirm.exchange")
                .durable(true) //设置持久化
                .alternate("backup.exchange")//设置备份交换机
                .build();
    }

    // 声明正常队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable("confirm.queue").build();
    }

    // 声明正常队列绑定正常交换机
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }

    //1、声明备份 Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
        return ExchangeBuilder.fanoutExchange("backup.exchange").build();
    }

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

    //3、备份队列绑定备份交换机
    @Bean
    public Binding backupBinding(@Qualifier("backQueue") Queue backQueue,
                                 @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(backQueue).to(backupExchange);
    }

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

    //5、警告队列绑定备份交换机
    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue warningQueue,
                                  @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(warningQueue).to(backupExchange);
    }
}

生产者代码

package com.leo.controller;

import com.leo.callback.MyCallBack;
import com.leo.callback.MyReturnsCallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;


@Slf4j
@RestController
@RequestMapping("/confirm")
public class ProducerController {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    //1、获取回调接口实现类对象
    @Autowired
    private MyCallBack myCallBack;

    @Autowired
    private MyReturnsCallback myReturnsCallback;
    //2、依赖注入 rabbitTemplate 之后再设置它的回调对象
    @PostConstruct
    public void init() {
        //设置交换机接收消息回调对象
        rabbitTemplate.setConfirmCallback(myCallBack);
        //设置队列接收消息失败后的回调对象信息
        rabbitTemplate.setReturnsCallback(myReturnsCallback);
    }

    /**
     * 发送消息
     *
     * @param message
     */
    @GetMapping("/sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {

        log.info("发送消息内容:{}", message);
        
        //3、测试备份交换机,将routingKey故意写错。
        CorrelationData correlationData4 = new CorrelationData("4"); //设置消息的唯一id 4
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey+2, "备份交换机测试", correlationData4);
    }
}

消费者代码

@Slf4j
@Component
public class ConfirmConsumer {
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    /**
     * 警告队列消费消息
     *
     * @param message
     */
    @RabbitListener(queues = WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message) {
        String msg = new String(message.getBody());
        log.info("接收到警告队列(warning.queue)的消息:{}", msg);
    }

}

结果 :警告消费者收到了消息。
在这里插入图片描述
细节点:
mandatory 参数(消息回退)与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。

1.8、优先级队列

作用:
   设置队列中消息消费的优先级。

实现原理:
   队列需要设置消息的优先级范围,消息需要设置消息的优先级。消费者需要等待消息已经发送到队列中,然后对队列中的消息进行排序,最后再去消费。

注意:
  队列设置消息的优先级最大级别范围是 0~255,等级超过这个会报错。不过一般工作中,这个级别我们会定义在0 ~10 ,太大会影响性能;级别数字越大,消费越优先。

配置类

package com.leo.config;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PriorityQueueConfig {

    // 声明队列,并设置消息的优先级范围为0~10
    @Bean
    public Queue priorityQueue() {
        return QueueBuilder.durable("priority").maxPriority(10).build();
    }
}

消息生产者

    @Test
    void contextLoads() {

        //发送十条消息到队列
        for (int i = 1; i < 11; i++) {
            //如果是第六条消息,将消息的权限设置为10,也就是最先消费。
            if (i == 6) {
                rabbitTemplate.convertAndSend("", "priority", "info" + i, corr -> {
                    corr.getMessageProperties().setPriority(10);
                    return corr;
                });

            } else {
                rabbitTemplate.convertAndSend("", "priority", "info" + i, corr -> {
                    corr.getMessageProperties().setPriority(5);
                    return corr;
                });
            }
            log.info("发出消息success:info" + i);
        }
    }

消费者代码

   /**
     * 测试优先级队列
     *
     * @param message
     */
    @Test
    //@RabbitListener(queues = "priority") //等发送完消息,然后在去掉该注释,然后再运行看结果
    public void receiveWarningMsg(Message message) {
        String msg = new String(message.getBody());
        log.info("接收到队列消息(priority)的消息:{}", msg);
    }

结果: 正常
在这里插入图片描述

补充
优先级队列有这个标识。
在这里插入图片描述

1.9、惰性队列

  RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
  默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

1.9.1、实现方式

  队列具备两种模式:default 和 lazy。默认的为default 模式,在3.6.0 之前的版本无需做任何变更。lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare()方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
  在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示例中演示了一个惰性队列的声明细节:

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

1.9.2、内存开销对比

在这里插入图片描述
  在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用运行内存是 1.2GB,而惰性队列仅仅占用运行内存 1.5MB。

完结:2022-05-19 17:35

### RabbitMQ 使用指南和教程 #### 安装 RabbitMQ RabbitMQ 是一种基于 AMQP 协议的消息中间件,用于实现分布式系统的可靠消息传递。以下是安装 RabbitMQ 的基本流程: 1. **安装 Erlang** RabbitMQ 基于 Erlang 编程语言开发,因此需要先安装 Erlang 运行环境。可以通过包管理器或者下载官方二进制文件完成安装。 2. **安装 RabbitMQ Server** 下载并安装 RabbitMQ Server 软件包。对于 Linux 用户,可以使用以下命令: ```bash sudo apt-get install rabbitmq-server ``` 3. **启动服务** 启动 RabbitMQ 服务后,默认监听端口为 `5672`(AMQP 协议),Web 管理界面默认运行在 `15672` 端口上。 ```bash sudo systemctl start rabbitmq-server ``` 4. **启用 Web 管理插件** 可通过以下命令启用 RabbitMQ 提供的 Web 管理工具: ```bash sudo rabbitmq-plugins enable rabbitmq_management ``` --- #### 配置用户与权限 为了安全访问 RabbitMQ 实例,通常需要创建自定义用户并分配相应权限。 - 创建新用户: ```bash rabbitmqctl add_user rabbitmq 211314 ``` 此操作会新增名为 `rabbitmq` 的用户,并将其密码设为 `211314`[^1]。 - 设置用户角色: ```bash rabbitmqctl set_user_tags rabbitmq administrator ``` 将该用户的标签设定为管理员角色,使其拥有完全控制权[^1]。 - 授予用户权限: ```bash rabbitmqctl set_permissions -p "/" rabbitmq ".*" ".*" ".*" ``` 上述命令授予用户对根虚拟主机 `/` 中所有资源的操作权限[^1]。 - 查看现有用户及其角色: ```bash rabbitmqctl list_users ``` --- #### 集群配置 RabbitMQ 支持多种集群模式来提升可用性和性能。主要分为两类:普通模式和镜像模式。 - **普通模式** 在这种模式下,各节点独立存储队列中的数据和其他元信息(如交换机)。当客户端尝试消费某个不在当前连接节点上的消息时,目标节点会被请求转发所需的数据[^2]。 - **镜像模式** 对比之下,在镜像模式中,指定队列的内容将在多个节点间保持一致副本。即使部分成员失效,剩余存活节点仍能继续提供完整的服务功能[^2]。 > 注意事项:尽管镜像模式提高了可靠性,但也带来了额外开销——网络流量增加以及写入延迟上升等问题需被充分考虑进去。 --- #### 应用集成示例 假设要在一个 Java 或 Python 应用程序里利用 RabbitMQ 来发送/接收消息,则可能涉及以下几个步骤: 1. **声明交换器 (Exchange)** 和绑定关系: ```java channel.exchangeDeclare(exchangeName, "direct", true); channel.queueDeclare(queueName, true, false, false, null); channel.queueBind(queueName, exchangeName, routingKey); ``` 如此一来便完成了持久化队列及路由键关联工作[^3]。 2. 发布一条测试消息至上述已建立好的通道路径之中; 3. 订阅对应主题下的事件流以便实时捕获最新动态更新情况; --- #### 总结 以上涵盖了从基础安装到高级特性使用的整个过程概述。希望这些指导能够帮助您快速掌握如何部署与维护属于自己的 RabbitMQ 平台实例! 问题
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值