【RabbitMQ】RabbitMQ 快速入门(精讲)

文章目录

一、RabbitMQ 简介

rabbitMQ官网:https://www.rabbitmq.com/

1、是什么

参考文章:https://zhuanlan.zhihu.com/p/157112243

“消息队列”是在消息的传输过程中 保存消息的容器

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而群集和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

rabbitMQ与Erlang的对应版本:https://www.rabbitmq.com/which-erlang.html
在这里插入图片描述

Rabbit科技有限公司开发了RabbitMQ,并提供对其的支持。起初,Rabbit科技是LSHIFT和CohesiveFT在2007年成立的合资企业,2010年4月被VMware旗下的SpringSource收购。RabbitMQ在2013年5月成为GoPivotal的一部分。

RabbitMQ是一套开源(MPL)的消息队列服务软件,是由 LShift 提供的一个 Advanced Message Queuing Protocol (AMQP) 的开源实现,由以高性能、健壮以及可伸缩性出名的 Erlang 写成。

2、能干什么

消息队列是一种接受数据,接受请求、存储数据、发送数据等功能的技术服务。
在这里插入图片描述
其有如下的功能:

  1. 流量削峰:对于大并发,如果直接访问服务器,很容易引起服务器的宕机。当我们使用MQ接受访问的请求的时候,会很大程度上缓解高并发。但是这也带来了一个问题——访问速度慢的问题,但是总比宕机强吧。
    在这里插入图片描述

  2. 应用解耦:如果我们不通过MQ,而是让服务之间之间进行访问,那么这会带来一个问题。比如:订单系统会去调用支付系统、库存系统、物流系统,如果支付系统出了问题,那么整个业务都无法完成。如果我们使用MQ,那么MQ将会作为订单系统和支付系统、库存系统、物流系统之间的一个桥梁,当MQ发现支付系统出了问题,那么MQ会监督支付系统,直至其完成任务
    在这里插入图片描述

  3. 异步处理:我们也许会有这么一种需求,服务A去调用服务B,但是服务B的执行需要一定的时间,但是服务A需要知道服务B什么时候执行完成。没有MQ的时候,我们也许需要在一定的时间内去调用服务B的回调函数(问问服务B可以了吗),但是这种方式一点都不优雅。使用MQ可以轻松的解决中问题。
    在这里插入图片描述

比如你有一个数据要进行迁移或者请求并发过多的时候,比如你有10W的并发请求下订单,我们可以在这些订单入库之前,我们可以把订单请求堆积到消息队列中,让它稳健可靠的入库和执行。

3、有什么特点

可靠性∶RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。

灵活的路由∶在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ已经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。

扩展性∶多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。

高可用性∶ 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。

多种协议∶ RabbitMQ除了原生支持AMQP协议,还支持STOMP、MQTT等多种消息中间件协议

多语言客户端∶ RabbitMQ几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。

管理界面∶RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。

插件机制∶RabiMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。


二、RabbitMQ 安装

如果你没有docker的基础,那么你可以看我的docker专栏中的文章【点击查看】

1、Docker 安装

(1)yum 包更新到最新
yum update

(2)安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
yum install -y yum-utils device-mapper-persistent-data lvm2
     
(3)设置yum源为阿里云
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
     
(4)安装docker
yum install docker-ce -y
     
(5)安装后查看docker版本
docker -v
     
 (6) 安装加速镜像
sudo mkdir -p /etc/docker
     
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://0wrdwnn6.mirror.aliyuncs.com"]
}
EOF
     
sudo systemctl daemon-reload
     
sudo systemctl restart docker

2、在 Docker 中安装 RabbitMQ

传统的方式是先拉取镜像在运行容器,但是这种方式,我们还需要进入容器中去设置账户和密码。(RebbitMQ 默认的是guest,游客角色)

① 拉取rabbitMQ的镜像

docker pull rabbitmq:management

② 创建并运行容器

docker run -di --name=myrabbit -p 15672:15672 rabbitmq:management

③ 进入容器

docker exec -it myrabbit bash

④ 设置rabbitmq的用户名和密码

rabbitmqctl add_user admin(用户名) admin(密码)

⑤ 给用户设置角色

rabbitmqctl set_user_tags admin administrator

⑥ 给用户设置权限

rabbitmqctl set_permissions -p "/" admin ".\*" ".\*" ".\*"

 "/" 表示哪个virtualhost
 admin 表示给哪个用户设置
 ".\*" ".\*" ".\*" 分别表示支持配置、写、读

查看当前用户列表

rabbitmqctl list_users

我们可以一步完成账户、密码和角色的设置

docker run -di --name myrabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:management

--hostname:指定容器主机名称
--name:指定容器名称
-e RABBITMQ_DEFAULT_USER=admin:设置用户名为admin
-e RABBITMQ_DEFAULT_PASS=admin:设置密码为admin
-p:将mq端口号映射到本地或者运行时设置用户和密码

我们使用一步完成完成账户、密码和角色的设置的方式在docker安装RabbitMQ,使用上面的命令之后。
在这里插入图片描述
我们可以使用docker ps 查看docker容器的进程,发现rabbitMQ已经成功启动了
在这里插入图片描述

我们可以使用 http://你的IP地址:15672 访问rabbit控制台(management plugin)。
在这里插入图片描述
在这里插入图片描述

三、RabbitMQ 中的角色

角色:1:none(无)

不能访问management plugin

角色2:management(管理员):查看自己相关节点信息

列出自己可以通过AMQP登入的虚拟机

查看自己的虚拟机节点 virtual hosts的queues,exchanges和bindings信息

查看和关闭自己的channels和connections

查看有关自己的虚拟机节点virtual hosts的统计信息。包括其他用户在这个节点virtual hosts中的活动信息

角色3:Policymaker(决策者)

包含management所有权限

查看和创建和删除自己的virtual hosts所属的policies和parameters信息

角色4:Monitoring(监控者)

包含management所有权限

罗列出所有的virtual hosts,包括不能登录的virtual hosts

查看其他用户的connections和channels信息

查看节点级别的数据如clustering和memory使用情况

查看所有的virtual hosts的全局统计信息

角色5:Administrator(管理员)

最高权限

可以创建和删除virtual hosts

可以查看,创建和删除users

查看创建permisssions

关闭所有用户的connections


四、AMQP 协议

1、什么是AMQP

AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。

2、 为什么要用AMQP

我们的目标是实现一种在全行业广泛使用的标准消息中间件技术,以便降低企业和系统集成的开销,并且向大众提供工业级的集成服务。

我们的宗旨是通过AMQP,让消息中间件的能力最终被网络本身所具有,并且通过消息中间件的广泛使用发展出一系列有用的应用程序。

3、AMQP生产者流转过程

在这里插入图片描述

4、AMQP消费者流转过程

在这里插入图片描述

五、RabbitMQ 的核心组成部分

1、RabbitMQ的原理

RabbitMQ的四大核心:生产者(producer)、交换机(exchange)、队列(queue)、消费者(consumer)

是broker,不是breoker
核心概念

Server:又称Broker ,接受客户端的连接,实现AMQP实体服务。 安装rabbitmq-server

Connection:连接,应用程序与Broker的网络连接 TCP/IP/ 三次握手和四次挥手

Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道,客户端可以建立对各Channel,每个Channel代表一个会话任务。

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

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

Exchange:交换机,接受消息,根据路由键发送消息到绑定的队列。在RabbitMQ在没有指定exchange,那么会有默认的exchange(default exchange,默认模式为direct模式)(不具备消息存储的能力)

Bindings:Exchange和Queue之间的虚拟连接,binding中可以保护多个routing key。

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

Queue:队列:也成为Message Queue,消息队列,保存消息并将它们转发给消费者。

2、RabbitMQ整体架构

在这里插入图片描述

3、RabbitMQ的运行流程

在这里插入图片描述

六、RabbitMQ 的 Hello World

上面说了那么多,我们现在就来演示(简单模式)一个rabbitMQ的hello world。

1、java 原生依赖

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

2、编写生产者

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

public class Producer {
    public static void main(String[] args) throws Exception{
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 设置连接属性
        factory.setHost("192.168.174.135");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("admin");
        factory.setPassword("admin");

        final String QUEUE_NAME = "queue01";

		// 创建连接
        Connection producer = factory.newConnection();
        // 增加一个信道
        Channel channel = producer.createChannel();
        
        /**
         * queueDeclare : 声明队列
         * @param queue 队列名
         * @param durable 是否将消息持久化到本地
         * @param exclusive 是否开启独占模式(消息共不共享)
         * @param autoDelete 在最后一个队列消费完消息后,队列是否自动删除
         * @param arguments 队列的一些其他参数
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        
        String message = "hello studious tiger!";
        /**
         * basicPublish : 简单的发布消息
         * @param exchange 消息发布时候所使用的的交换机(有默认的交换机)
         * @param routingKey 路由关键字(我们现在没有,后面会讲到,现在直接写队列名即可)
         * @param mandatory true if the 'mandatory' flag is to be set
         * @param props 消息的一些其他参数,我们现在没有
         * @param body 消息体(byte类型)
         */
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));

        System.out.println("消息发送成功!");
        
        // 关闭资源
        if (channel != null && channel.isOpen()) {
            try {
                channel.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

执行
在这里插入图片描述
可以发现,存在了queue01,且就绪消息为1,说明消息发布成功
在这里插入图片描述

3、编写消费者

import com.rabbitmq.client.*;

public class Customer {
    public static void main(String[] args) throws Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.174.135");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("admin");
        factory.setPassword("admin");

        final String QUEUE_NAME = "queue01";

        Connection producer = factory.newConnection();
        Channel channel = producer.createChannel();

        /**
         * basicConsume :简单的消费消息
         * @param queue 队列名
         * @param autoAck autoAck设置为true时,消息队列可以不用在意消息消费者是否处理完消息,
         * 一直发送全部消息。但在公平分发中,也就是autoAck设置为false,在发送一个消息后到没收
         * 到消息消费者成功消费消息的信息回执之间,是不会继续给这个消息继续发送消息的。
         * @param deliverCallback 当消息被接收时执行此回调函数
         * @param cancelCallback 当消费者被取消时执行此回调函数
         */
        // 拉姆达斯表示式(DeliverCallback是函数式接口,我们使用拉姆达斯表达式之后,就不需要使用实现类了)
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println( new String(message.getBody(),"UTF-8"));
        };
        // 拉姆达斯表示式
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("消息中断...");
        };
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
        
        // 关闭资源
        if (channel != null && channel.isOpen()) {
            try {
                channel.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

注意:以为上面使用了8 - Lambdas(拉姆达斯)表达式,你要保证是1.8的环境
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

执行
在这里插入图片描述
可以发现,就绪消息为0,说明消费成功
在这里插入图片描述

3、抽取工具类

我们可以发现每次都要重复的建立信道和关闭资源,很是麻烦,我们可以将其抽取成工具类

public class RabbitMQUtils {
    /**
     * @return 返回信道 (Channel)
     * @throws Exception
     */
    public static Channel getChannel() throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.174.135");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("admin");
        factory.setPassword("admin");
        Connection producer = factory.newConnection();
        return producer.createChannel();
    }

    /**
     * 关闭资源
     * @param channel 信道
     * @param connection 连接
     */
    public static void closeSource(Channel channel,Connection connection) {
        if (channel != null && channel.isOpen()) {
            try {
                channel.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

七、消息应答【简单模式 work】

1、介绍

书接上文,在上文中我们将 autoAck设置为true ,但是这种方法其实是不安全的,因为当消费者接受到队列的消息后,将会立即给队列发送应答消息,队列将删除该条消息。这就会导致一个问题:当消费者接受消息后没有完成其对应的任务,又因为队列中已经将消息删除了,消费者的任务将彻底的执行失败!

所以我们通常会使用手动应答

channel.basicAck(long deliveryTag, boolean multiple):表示应答

channel.basicNack(long deliveryTag, boolean multiple, boolean requeue):表示拒绝应答

channel.basicReject(long deliveryTag, boolean requeue):表示拒绝应答

basicNack 和 basicReject的区别:

requeue 为true时表示消息重新排队,消息将会被再次推送。

从参数上我们也可以看出来,在basicNack 中是多了一个 boolean multiplemultiple一次是一次性拒绝多条应答(例如:basicNack(5, true, true)表示拒绝1、2、3、4、5条消息,且消息重新排队;basicNack(5, true, true)表示拒绝第5条消息,且消息重新排队)

消息队列的重新应答机制示意图:
在这里插入图片描述

2、代码实现

接下来我们做一个实验:我们需要一个 生产者两个消费者(消费者1处理业务时间为1秒,消费者2处理业务的时间为30秒),生产者用于发送消息,消费者用于接收消息。因为exchange默认使用的是轮询机制,所以当 生产者 的生产的aa、bb、cc、dd四个消息时,应该是aa和cc由一个 消费者 接受,bb和dd由 另一个消费者 接受。

那是当 消费者2 接收第二条消息时,我们人为的制造宕机,导致 消费者2 无法继续接受消息,我们来验证当 消费者2 没有应答队列时,原本应该给 消费者2 的消息会不会发给 消费者1

生产者代码

public class Producer {
    public static  final String ACK_QUEUE = "ack_queue";

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


        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(ACK_QUEUE,false,false,false,null);

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",ACK_QUEUE,null,message.getBytes("UTF-8"));
            System.out.println("成功发送消息:"+message);
        }

        RabbitMQUtils.closeSource(channel,connection);
    }
}

消费者01代码

public class Customer01 {
    public static  final String ACK_QUEUE = "ack_queue";

    public static void main(String[] args) throws Exception {
        System.out.println("Customer01等待接受消息队列中的消息...");

        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        DeliverCallback deliverCallback = (consumerTag, message)->{
            // 休眠1秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( new String(message.getBody(),"UTF-8"));

            // 手动应答
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("消息中断...");
        };
        // 不自动应答
        Boolean autoAck = false;
        channel.basicConsume(ACK_QUEUE,autoAck,deliverCallback,cancelCallback);

        RabbitMQUtils.closeSource(channel,connection);
    }
}

消费者02代码

public class Customer02 {
    public static  final String ACK_QUEUE = "ack_queue";

    public static void main(String[] args) throws Exception {
        System.out.println("Customer02等待接受消息队列中的消息...");

        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        DeliverCallback deliverCallback = (consumerTag, message)->{
            // 休眠10秒钟
            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( new String(message.getBody(),"UTF-8"));

            // 手动应答
            channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
        };
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("消息中断...");
        };
        // 不自动应答
        Boolean autoAck = false;
        channel.basicConsume(ACK_QUEUE,autoAck,deliverCallback,cancelCallback);

        RabbitMQUtils.closeSource(channel,connection);
    }
}

生产者发送dd时候,我将消费者2关闭,实现结果显示dd被消费者1接收处理了(dd原本应该是消费者2接收的)。说明七-1中的理论是正确的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、不公平分发策略

rabbitmq 默认使用的是轮询分发策略,这种策略有一个明显的缺点——不能实现“能者多劳”。所以一般情况下我们使用不公平分发策略的情况还是比较多的。

实现不公布分发我们只需要在消费端设置channel.basicQos(1)即可,channel.basicQos(0)是默认的轮询分发。
在这里插入图片描述
基于七-2的代码实现了能者多劳
在这里插入图片描述

4、预取值

我们通过前面的学习可以知道,队列中的消息传递给消费之是通过信道(channel),所谓的预取值就像是给信道设置容量似的。使用预取值需要开启autoAck=false,需要使用basciAck进行确认消息


官方给出的解释

For cases when multiple consumers share a queue, it is useful to be able to specify how many messages each consumer can be sent at once before sending the next acknowledgement. This can be used as a simple load balancing technique or to improve throughput if messages tend to be published in batches. For example, if a producing application sends messages every minute because of the nature of the work it is doing.

Note that RabbitMQ only supports channel-level prefetch-count, not connection or size based prefetching.

对于多个使用者共享一个队列的情况,在发送下一个确认之前,可以指定每个使用者一次可以发送多少消息。这可以用作一种简单的负载平衡技术,或者在消息倾向于成批发布时提高吞吐量。例如,如果一个生产应用程序由于其所做工作的性质而每分钟发送一次消息。

请注意,RabbitMQ仅支持通道级预取计数,不支持基于连接或大小的预取。


在不公平分发中,我们是通过设置channel.basicQos(1)实现的,对于预取值,我们也是通过channel.basicQos(count)进行实现的,只不过这里的count不再是1或0,而是其他的数字(例如我们可以使用channel.basicQos(2)、channel.basicQos(5))。
在这里插入图片描述
上图中也不是绝对的,它只不过是极大可能性,比如消息的总数为10条,其内部有一个伏安法,根据prefetchcount的设置来尽量的平衡这10条消息(负载均衡)。上图中只是为例让大家更加形象的理解(不是真成的原理)。

基于七-2的代码实现了预取值的演示

我们会发送7条消息,不出意外的话,消费者01中将会接受2条消息(处理业务的时间为1秒),消费者02中将会接受5条消息(处理业务时间为30秒)

消费者01设置预取值为2
在这里插入图片描述
消费者02设置预取值为5
在这里插入图片描述

八、消息持久化

1、介绍

我们上面并没有实现队列和消息的持久化,这样实际上是不明智的,因为当rabbitmq因意外情况导致宕机,那么在rabbitmq中的队列和队列中的消息都将不复存在。在实际的开发中一般是很少使用非持久化的消息队列的,因为这样会极大的增加了丢消息的风险,我们并不能保证我们的rabbitmq是绝对可靠的。因为对于队列和队列中的消息的持久化是十分有必要的。

如果你有印象的话,我们上面在生产者申明队列的时候,给声明队列的方法(queueDeclare)设置了三个false,其中第一个是表示队列不持久化。也就是说,当我们将原本的false改为true,就实现了队列的持久化。

2、队列的持久化

没有持久化前的队列:
在这里插入图片描述

持久化之后的队列:(一个队列在声明之后,其特点就不可改变,除非删去重新声明)

删除原来队列
在这里插入图片描述
生产者端声明队列时实现持久化
在这里插入图片描述
在这里插入图片描述

3、消息的持久化

对于消息的也是在生产者中进行声明的,如果你有印象的话,在发布消息的方法(basicPublish)中的第三个参数(BasicProperties props)我们设置的null,我们只需要使用 MessageProperties.PERSISTENT_TEXT_PLAIN 替换 null 即可实现消息的持久化。

在这里插入图片描述

4、发布确认【简单模式 work】

上面我们开启了消息的持久化,但是仍然会存在这么一种情况,当生产者通过交换机将消息出传递给队列,如果在这个期间消息传递失败,也是会影响到服务的健壮性的。我们需要一种机制:生产者可以知道消息是否发布成功(当消息发布失败时,可以个性化处理)
在这里插入图片描述

开启发布确认

生产者可以使用 channel.confirmSelect() 开启发布确认,使用 channel.waitForConfirms() 接受到发布确认。

③ 单个发布确认

所谓的单个发布确认就是生产者每次向队列中发布一条消息,都会等待队列发布确认。这样可以清晰的知道每次发布是否成功,但是这种模式带来的问题是影响效率
在这里插入图片描述

代码:

/**
* 单个确认发布
* @throws Exception
*/
public static void publishMessageSingle() throws Exception {
    Connection connection = RabbitMQUtils.getConnection();
    Channel channel = connection.createChannel();
    // 队列名
    String queueName= UUID.randomUUID() + "";
    // 声明队列
    channel.queueDeclare(queueName,false,false,false,null);
    // 开启发布确认(别忘记***)
    channel.confirmSelect();

    // 开始时间
    long begin = System.currentTimeMillis();
    int messageCounts = 1000;
    // 发送1000条消息
    for (int i = 0; i < messageCounts; i++) {
        String message = i+1+"";
        channel.basicPublish("",queueName,null,message.getBytes("UTF-8"));

        // 向队列单个确认
        boolean tags = channel.waitForConfirms();
        // 判断是否发送成功
        if (tags){
            System.out.println("第" + message + " 个消息成功发送...");
        }else {
            System.out.println("第" + message + " 个消息成功失败...");
        }
    }
    // 结束时间
    long end = System.currentTimeMillis();
    
    System.out.println("【publishMessageSingle耗时 "+(end-begin)+" ms】");
}

在这里插入图片描述
完成发布的时间是1123毫秒
在这里插入图片描述

② 批量发布确认

所谓的批量发布确认就是设置一个批量值,当发布的消息的数量每次满足批量值得时候,进行一次发布确认。这样的模式相比单个发布确认模式效率是大大的提高了,但是带来的问题是无法确保每次发布是否成功。
在这里插入图片描述

代码:

/**
* 批量确认发布
* @throws Exception
*/
public static void publishMessageBatch() throws Exception {
    Connection connection = RabbitMQUtils.getConnection();
    Channel channel = connection.createChannel();
    // 队列名
    String queueName= UUID.randomUUID() + "";
    // 声明队列
    channel.queueDeclare(queueName,false,false,false,null);
    // 开启发布确认(不要忘记开启***)
    channel.confirmSelect();

    // 开始时间
    long begin = System.currentTimeMillis();
    int messageCounts = 1000;
	// 设置batch值为100
    int batchSize = 100;
    // 发送1000条消息
    for (int i = 0; i < messageCounts; i++) {
        int message = i+1;
        channel.basicPublish("",queueName,null,(message+"").getBytes("UTF-8"));

        // 批量确认(batch 为 100)
        if (message%batchSize==0){
            // 确认
            boolean tags = channel.waitForConfirms();
            // 判断是否发送成功
            if (tags){
                System.out.println("第" + message/100 + " 批消息成功发送...");
            }else {
                System.out.println("第" + message/100 + " 批消息成功失败...");
            }
        }
    }
    // 结束时间
    long end = System.currentTimeMillis();
    
    System.out.println("【publishMessageBatch耗时 "+(end-begin)+" ms】");
}

在这里插入图片描述
完成发布的时间是64毫秒,明显比的单个发布确认的效率高
在这里插入图片描述

③ 异步发布确认

所谓的异步发布就是我们将消息封装到类似map的数据结构中(rabbitmq自己封装),这样的好处是每条消息可以有编号(deliveryTag),在发布的时候,我们是一次性全部发布,对于发布确认来说是异步的,在异步程序中检测到某一个消息发布失败,可以返回消息的编号给生产者,以便以生产者可以准确的知道哪次发布失败。异步发布确认是最复杂的一种模式,但是这样模式是性价比最高的,在这种模式中在保证效率的同时可以清晰的知道每次发布是否成功。

在这里插入图片描述

rabbitmq给channel提供了一个发布确认监听器 channel.addConfirmListener(ackCallback,nackCallback) 其既可以实现异步发布确认。我们需要通过 拉姆达斯表示式写两个回调函数ackCallback(发布成功时调用的回调函数)和nackCallback(发布失败时调用的回调函数)。

代码:

/**
 * 异步确认发布
 * @throws Exception
 */
public static void publishMessageAsync() throws Exception {
    Connection connection = RabbitMQUtils.getConnection();
    Channel channel = connection.createChannel();
    // 队列名
    String queueName = UUID.randomUUID() + "";
    // 声明队列
    channel.queueDeclare(queueName,false,false,false,null);
    // 开启发布确认
    channel.confirmSelect();

    // 开始时间
    long begin = System.currentTimeMillis();
    int messageCounts = 1000;

    // 发布成功时调用的回调函数
    // deliveryTag:消息的标记
    // multiple:是否批量确认
    com.rabbitmq.client.ConfirmCallback ackCallback = (deliveryTag,multiple)->{
        System.out.println("消息 " + deliveryTag + " 发布成功...");
    };
    // 发布失败时调用的回调函数
    ConfirmCallback nackCallback = (deliveryTag,multiple)->{
        System.out.println("消息 " + deliveryTag + " 发布失败...");
    };
    /**
     * 设置一个发布确认监听器
     * ackCallback : 发布成功时的监听
     * nackCallback : 发布失败时的监听
     */
    channel.addConfirmListener(ackCallback,nackCallback);

    // 发送1000条消息
    for (int i = 0; i < messageCounts; i++) {
        int message = i+1;
        channel.basicPublish("",queueName,null,(message+"").getBytes("UTF-8"));
    }
    long end = System.currentTimeMillis();
    
    System.out.println("publishMessageAsyn耗时 "+(end-begin)+" ms");

}

在这里插入图片描述
发现确实是异步的,而且能够保证效率。
在这里插入图片描述

问题:如何处理异步未确认消息?

最好的范范就是将为确认的消息存放到一个基于内存的能够被发布线程访问的队列。比如用类似ConcurrentLinkedQueue(并发链路式队列)这个队列在confirm callbacks 与发布进程之间进行通信。

代码:

/**
 * 异步确认发布
 * @throws Exception
 */
public static void publishMessageAsync() throws Exception {
    Connection connection = RabbitMQUtils.getConnection();
    Channel channel = connection.createChannel();
    // 队列名
    String queueName = UUID.randomUUID() + "";
    // 声明队列
    channel.queueDeclare(queueName,false,false,false,null);
    // 开启发布确认
    channel.confirmSelect();

    /**
     * 【1】线程安全有序的一个哈希表,使用与高并发,用于存储发布的消息
     * 1. 轻松的将序号与消息进行关联
     * 2. 根据序号可以批量的删除条目
     * 3. 支持高并发(多线程)
     */
    ConcurrentSkipListMap<Long, String> messageMap = new ConcurrentSkipListMap<>();

    // 发布成功时调用的回调函数
    // deliveryTag:消息的标记
    // multiple:是否批量确认
    com.rabbitmq.client.ConfirmCallback ackCallback = (deliveryTag,multiple)->{
        // 【3.1】如何是批量,则批量删除
        if (multiple){
            // headMap(K toKey) 方法用于返回此映射的键严格小于toKey的部分视图。
            // 其实就是获取deliveryTag之前的内容
            ConcurrentNavigableMap<Long, String> map = messageMap.headMap(deliveryTag);
            map.clear();
        }else {
            // 不是批量则直接删除即可
            messageMap.remove(deliveryTag);
        }
        
        System.out.println("消息 " + deliveryTag + " 发布成功...");
    };
    // 发布失败时调用的回调函数
    ConfirmCallback nackCallback = (deliveryTag,multiple)->{
        // 【3.2】
        String message = messageMap.get(deliveryTag);
        System.out.println("消息: " + message + "【序号:" + deliveryTag + "】 ,发布失败...");
    };
    /**
     * 设置一个发布确认监听器
     * ackCallback : 发布成功时的监听
     * nackCallback : 发布失败时的监听
     */
    channel.addConfirmListener(ackCallback,nackCallback);

    // 开始时间
    long begin = System.currentTimeMillis();
    int messageCounts = 1000;
    // 发送1000条消息
    for (int i = 0; i < messageCounts; i++) {
        int message = i+1;
        channel.basicPublish("",queueName,null,(message+"").getBytes("UTF-8"));
        // 【2】将发送的消息记录在map中 channel.getNextPublishSeqNo()表示下一个发送消息的序号
        messageMap.put(channel.getNextPublishSeqNo(),message+"");
    }

    long end = System.currentTimeMillis();
    System.out.println("publishMessageAsyn耗时 "+(end-begin)+" ms");

}

九、RabbitMQ 常用的模式(!!!)

1、java 原生

java 原生依赖

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

值得注意的是,在程序中我们既可以在生产者端定义交换机,也可以在消费端定义交换机。但是独一队列,我们只能早消费端进行定义,因为需要一个消费者对应一个队列。但是,我还是建议交换机的声明与队列的声明可以放在一起,因为生产者发布消息是要通过交换机发送到队列中的,如果你的交换机和队列的声明不是同时存在的,那么发送第一次消息或接收第一次消息时是会丢失的。

① Fanout 模式

发布及订阅模式,当多个队列(queue)绑定(bin)同个交换机(exchange)时,当这个交换机在转发消息的时候,所有绑定它的队列都会收到消息。然后每一个队列对应一个消费者,这样一个生产者发布的消息就可以被多个消费者同时收到。
在这里插入图片描述

生产者:

public class Producer {
    public static final String EXCHANGE_NAME = "myExchange_fanout";
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明交换机(名称和类型)
        channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.FANOUT);

        // 发布消息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
            System.out.println("【Producer】消息发布成功:"+message);
        }
    }
}

消费者01:

public class Customer01 {
    public static final String EXCHANGE_NAME = "myExchange_fanout";
    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列(这种方式是临时队列,队列名随机)
        String queue = channel.queueDeclare().getQueue();
        // 绑定交换机
        channel.queueBind(queue,EXCHANGE_NAME,"");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer01】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer01】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);
    }
}

消费者02:

public class Customer02 {
    public static final String EXCHANGE_NAME = "myExchange_fanout";
    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列(这种方式是临时队列,队列名随机,当消息被消费之后,队列自动删除)
        String queue = channel.queueDeclare().getQueue();
        // 绑定交换机
        channel.queueBind(queue,EXCHANGE_NAME,"");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer02】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer02】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume(queue,true,deliverCallback,cancelCallback);
    }
}

在这里插入图片描述


② Direct 模式

direct 模式和 fanout 模式的区别在于添加了一个限制条件(routing key)(你可以认为是路由,标签,分类…),通过这个限制条件,即使多个队列(queue)绑定了同一个交换机(exchange),我们也可以指定该交换机(exchange)在转发消息的时候,将消息转发给指定的队列(queue)。【注意:同一个队列可以添加多个路由key】
在这里插入图片描述

接下来,我们将实现如下这个模型,消费者01的 routing key 为 info 和 warning。消费者02的 routing key 为 error。
在这里插入图片描述生产者:

public class Producer {
    public static final String EXCHANGE_NAME = "myExchange_direct";
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明交换机(名称和类型)
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        // 发布消息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String messageAll = scanner.next();
            // 将输入的内容根据“-”进行切分,得到message (info[0]) 和 routing king (info[1])
            String[] info = messageAll.split("-");
            channel.basicPublish(EXCHANGE_NAME,info[1],null,info[0].getBytes("UTF-8"));
            System.out.println("【Producer】消息发布成功:"+info[0]);
        }
    }
}

消费者01(routing key为 info 和 warning):

public class Customer01 {
    public static final String EXCHANGE_NAME = "myExchange_direct";
    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare("queue_direct01",false,false,false,null);
        // 绑定交换机(routing key为 info 和 warning)
        channel.queueBind("queue_direct01",EXCHANGE_NAME,"info");
        channel.queueBind("queue_direct01",EXCHANGE_NAME,"warning");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer01】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer01】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume("queue_direct01",true,deliverCallback,cancelCallback);
    }
}

消费者02(routing key为error):

public class Customer02 {
    public static final String EXCHANGE_NAME = "myExchange_direct";
    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare("queue_direct02",false,false,false,null);
        // 绑定交换机(routing key为 error)
        channel.queueBind("queue_direct02",EXCHANGE_NAME,"error");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer02】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer02】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume("queue_direct02",true,deliverCallback,cancelCallback);

    }
}

在这里插入图片描述


③ Topic 模式

话题模式,从原理图中,我们可以发现,topic 模式和Direct 模式的区别在于topic模式可以尽心模糊匹配。怎么理解模糊匹配呢?其实和正则表达式类似(但是要比正则表达式简单)。
在这里插入图片描述
在 topic 模式下的限制条件(routing key)是可以进行模糊匹配的,例如 *.orange.* *.*.rabbit lazy.#

.:表示级的分界

*:表示可以模糊匹配1级(只代表一个单词)

#:表示可以模糊配0级或多级(可代表多个单词,也可以没有)

案例:

① 如果交换机在转发消息时添加的限制调剂是 lazy.xxxx.xxxx.xxxx ,那么只有限制条件(routing key)为 lazy.# 的队列可以收到消息,因为 # 表示可匹配0级或多级。

② 如果交换机在转发消息时添加的限制调剂是 lazy.orange.xxxx ,那么只有限制条件(routing key)为 lazy.#*.orange.* 的队列可以收到消息。

③ 如果交换机在转发消息时添加的限制调剂是 lazy.orange.xxxx.xxxx ,那么只有限制条件(routing key)为 lazy.# 的队列可以收到消息。之所以 *.orange.* 的队列收不到消息是因为 orange下面有且只能有1级,很明显 lazy.orange.xxxx.xxxxorange 下面有2级。

④ 如果交换机在转发消息时添加的限制调剂是 lazy.orange.rabbit ,那么只有限制条件(routing key)为lazy.#*.orange.**.*.rabbit 的队列可以收到消息。

下面,我们根据上面的案例进行代码实现

生产者:

public class Producer {
    public static final String EXCHANGE_NAME = "myExchange_topic";
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明交换机(名称和类型)
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

        // 将 路由 和 消息封装到 map 中
        HashMap<String, String> routingkeyMessageMap = new HashMap<>();
        routingkeyMessageMap.put("lazy.xxxx.xxxx.xxxx","本消息将被Customer02接收");
        routingkeyMessageMap.put("lazy.orange.xxxx","本消息将被Customer01和Customer02接收");
        routingkeyMessageMap.put("lazy.orange.xxxx.xxxx","本消息将被Customer02接收");
        routingkeyMessageMap.put("lazy.orange.rabbit","本消息将被Customer01和Customer02接收接收");

        for (Map.Entry<String, String> map : routingkeyMessageMap.entrySet()) {
            String routingKey = map.getKey();
            String message = map.getValue();
            // 发布消息
            channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));
            System.out.println("【Producer】消息发布成功:"+message);
        }
    }
}

消费者01:(routing key为 .orange.

public class Customer01 {
    public static final String EXCHANGE_NAME = "myExchange_topic";
    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare("queue_topic01",false,false,false,null);
        // 绑定交换机(routing key为 *.orange.*)
        channel.queueBind("queue_topic01",EXCHANGE_NAME,"*.orange.*");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer01】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer01】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume("queue_topic01",true,deliverCallback,cancelCallback);
    }
}

消费者02:(routing key为 ..rabbit 和 lazy.#)

public class Customer02 {
    public static final String EXCHANGE_NAME = "myExchange_topic";
    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare("queue_topic02",false,false,false,null);
        // 绑定交换机(routing key为 *.*.rabbit 和 lazy.#)
        channel.queueBind("queue_topic02",EXCHANGE_NAME,"*.*.rabbit");
        channel.queueBind("queue_topic02",EXCHANGE_NAME,"lazy.#");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer02】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer02】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume("queue_topic02",true,deliverCallback,cancelCallback);

    }
}

在这里插入图片描述


④ Work 模式

当存在大量的消息时,我们希望有多个工作线程同时执行消息,为了避免重复工作,每一个消息只能被处理一次,不能被处理多次,这就引出了两种分发机制——轮询模式和公平分发。
在这里插入图片描述

轮询模式(在 【七】 和【八】中已经实现)

公平分发(在 【七】 和【八】中已经实现)


⑤ Header 模式

header模式,这种模式也是比较简单的,交换机(exchange)在发送消息的时候需要通过参数来判断消息发给哪个队列。比如:队列1(queue01)中的header为 x=1,队列2(queue02)中的header为 y=1,那么如果交换机(exchange)在发送消息的时候的条件为 x=1,那么只有队列1(queue01)会受到消息。


十、死信队列 与 延迟队列

1、死信队列

什么是死信队列?死信队列 一般来说,producer将消息投递到queue中,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了 死信 (Dead Letter),所有的 死信 都会放到 死信队列 中。

死信队列的来源:消息TTL(Time to Liv 生存时间)过期队列达到最大长度消息被拒绝(basic.regect 或 basic.nack)并且requeue=false开启手动应答(autoAck为false)

架构图

在这里插入图片描述

argument的key
在这里插入图片描述

① 因为消息超时导致的消息转发死信队列

下面演示因为消息超时(TTL)导致的消息转发死信队列

我们创建customer01,实现上面架构图中的两个交换机两个队列以及绑定关系

public class Customer01 {

    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 {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 声明正常交换机和死信交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明正常队列
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); // 设置正常队列对应的死信交换机
        arguments.put("x-dead-letter-routing-key","tiger"); // 设置死信交换机的routing key
        //arguments.put("x-message-ttl",10000); // 设置消息的过期时间(单位 ms)
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 绑定正常的交换机与正常的队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"studious");
        // 绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"tiger");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer01】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer01】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
    }
}

创建消费者(producer):

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

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

        // 设置消息的过期时间(单位 ms) expiration:到期
        AMQP.BasicProperties props = new AMQP.BasicProperties().builder().expiration("10000").build();

        // 发布消息
        for (int i = 0; i <10 ; i++) {
            String message = "消息"+(i+1);
            channel.basicPublish(NORMAL_EXCHANGE,"studious",props,message.getBytes("UTF-8"));
            System.out.println("【Producer】消息发布成功:"+message);
        }
    }
}

下面,我们先运行customer01,实现两个交换机两个队列以及绑定关系,然后关闭customer01。然后运行producer,向队列中发送消息,因为没有任何的消费者接受,所以10秒中之后,消息就过期了,原本在normal_queue中的10条消息将会转发到dead_queue中去。演示如下:
请添加图片描述

创建customer01:

public class Customer02 {
    public static final String DEAD_QUEUE = "normal_queue";

    public static void main(String[] args) throws Exception {
        // 建立连接,开启信道
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer02】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer02】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback);
    }
}

其实到这里就已经结束了,我们的实验的目的就是测试死信队列。


② 因为队列满导致的消息转发死信队列

② 下面演示因为队列满导致的消息转发死信队列
我们需要在正常队列中设置一个队列的最大长度(注意我们并不需要设置队列的过期时间),这样的话,当正常的队列满时,多余的消息都会进入死信队列。

生产者代码(并不需要设置过期时间)

public class Producer {
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        
        // 发布消息
        for (int i = 0; i <10 ; i++) {
            String message = "消息"+(i+1);
            channel.basicPublish(NORMAL_EXCHANGE,"studious",null,message.getBytes("UTF-8"));
            System.out.println("【Producer】消息发布成功:"+message);
        }
    }
}

消费者01代码(设置队列的最大长度):

这就是与 ① 中的customer01的唯一的区别。
在这里插入图片描述

public class Customer01 {

    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 {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 声明正常交换机和死信交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明正常队列
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); // 设置死信交换机
        arguments.put("x-dead-letter-routing-key","tiger"); // 设置死信交换机的routing key
        //----------------------设置队列的最大长度-----------------------//
        arguments.put("x-max-length",6); // 设置正常的队列的最大长度是6
        //-------------------------------------------------------------//
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 绑定正常的交换机与正常的队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"studious");
        // 绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"tiger");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("【Customer01】接收消息成功:"+ new String(message.getBody(),"UTF-8"));
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer01】接收消息失败...");
        };

        // 消费消息
        channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
    }
}

我们将原本的队列删除,然后启动customer01,重新搭建两个交换机两个队列以及绑定关系。然后关闭costomer01(如果costomer01一直开着,那么producer发布的消息直接被消费了,这样就没有意义了),启动producer发布10条消息(因为正常队列的最大长度为6,所以会有4个消息进入死信队列)。

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


③ 因为消息被拒绝导致的消息转发死信队列

③ 下面演示因为消息被拒绝导致的消息转发死信队列

在消费者中设置自动应答(autoAck:false);拒绝接收(channel.basicReject);不重新放入队列(requeue:false)。
在这里插入图片描述
在这里插入图片描述

消费者代码(设置手动应答,设置拒绝接收别不重新放入队列)

public class Customer01 {

    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 {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        // 声明正常交换机和死信交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明正常队列
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); // 设置死信交换机
        arguments.put("x-dead-letter-routing-key","tiger"); // 设置死信交换机的routing key
        //arguments.put("x-max-length",6); // 设置正常的队列的最大长度是6
        //arguments.put("x-message-ttl",10000); // 设置消息的过期时间(单位 ms)
        channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE,false,false,false,null);

        // 绑定正常的交换机与正常的队列
        channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"studious");
        // 绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"tiger");

        // 接收成功的回调函数
        DeliverCallback deliverCallback = (consumerTag, message)->{
            String msg = new String(message.getBody(), "UTF-8");
            if ("消息8".equals(msg)){
                // 【拒绝接收】:channel.basicReject;【不重新放入队列】:requeue:false
                channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
                System.out.println("【Customer01】接收消息【失败】:"+ new String(message.getBody(),"UTF-8"));
            }else{
                System.out.println("【Customer01】接收消息【成功】:"+ new String(message.getBody(),"UTF-8"));
            }
        };
        // 接收失败的回调函数
        CancelCallback cancelCallback = consumerTag ->{
            System.out.println("【Customer01】接收消息失败...");
        };

        // 消费消息(【设置手动应答】 autoAck:false)
        channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,cancelCallback);
    }
}

我们将原本的队列删除,然后启动customer01,重新搭建两个交换机两个队列以及绑定关系。然后启动producer发布10条消息(因为消费者01拒绝接收“消息8”,所以会有一个消息进入死信队列,其余的9条被成功消费)。
在这里插入图片描述
在这里插入图片描述

2、延迟队列

所谓的延迟队列就是被设置了过期时间消息所在的队列(例如上面的normal_queue队列)。我们可以通过两个方面去理解它,因为通过上面的学习,我能知道设置消息的过期时间的方式有两种:

  1. 第一种,在创建队列的时候设置整个队列中的消息全部延迟指定的时间,这种方式不够灵活,但是也是有使用场景的。
    在这里插入图片描述

  2. 第二种,在发送消息的时候,为消息设置过时时间,同多个生产者通过一个交换机发布消息时,就可以实现一个队列中有不同的过期时间的消息,比较灵活。
    在这里插入图片描述

延迟队列的使用场景:

  • 订单10分钟之后未支付,自动取消。
  • 创建新店铺,10天内没有发布商品,则自动发送消息提醒。
  • 新用户注册后,如果3天内没有登录,进行短信提醒。
  • 用户发起退款,如果3天后没有处理,则通知管理员处理。
  • 预定会议,需要提前10分钟通知参会人员。

我们会在【十一】中实现延迟队列


十一、整合 springboot

springboot依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

1、实现【十、2】中的延迟队列(设置队列的延迟时间)

声明整个队列的过期时间一致,这样所有的消息只要进入队列都有相同的延迟时间,这样并不灵活

模型
在下面的模型中我们可以发现存在2个交换机(exchange_X、exchange_Y)和3个队列(queue_a、queue_b、queue_d)和一个生产者(producer)和一个消费者(customer)。其中queue_a、queue_b属于延迟队列(延迟时间分别为10s和20s),queue_d是死信队列
在这里插入图片描述
第一步:配置连接配置文件

# 服务端口
server:
  port: 8080

# 配置rabbitmq服务
spring:
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: 192.168.174.136
    port: 5672

第二步:声明交换机
不要导错包了哈

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;

@Configuration
public class rabbitmqConfig {
    // 声明交换机名
    public static final String EXCHANGE_X = "exchange_X";
    public static final String DEAD_LETTER_EXCHANGE_Y = "exchange_Y";
    
    // 声明队列名
    public static final String QUEUE_A = "queue_a";
    public static final String QUEUE_B = "queue_b";
    public static final String DEAD_LETTER_QUEUE_D = "queue_d";

    /**
     * 声明 交换机 exchange_X
     * 第一个false表示不持久化
     * 第二个false表示不自动删除
     * @return
     */
    @Bean("exchange_X")
    public DirectExchange exchange_X(){
        return new DirectExchange(EXCHANGE_X,false,false);
    }

    /**
     * 声明 死信交换机 exchange_Y
     * 第一个false表示不持久化
     * 第二个false表示不自动删除
     * @return
     */
    @Bean("exchange_Y")
    public DirectExchange exchange_Y(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE_Y,false,false);
    }

第三步:声明队列

    /**
     * 声明 延迟队列 queue_a
     * x-dead-letter-routing-key 要和 死信队列绑定死信交换机的routing key 一致,要不死信队列收不到消息
     * @return
     */
    @Bean("queue_a")
    public Queue queue_a(){
        Map<String, Object> arguments = new HashMap<>();
        // 设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE_Y);
        // 设置死信routing key = dead
        arguments.put("x-dead-letter-routing-key","dead");
        // 设置TTL过期时间
        arguments.put("x-message-ttl",10000);
        return QueueBuilder.nonDurable(QUEUE_A).withArguments(arguments).build();
    }

    /**
     * 声明 延迟队列 queue_b
     * x-dead-letter-routing-key 要和 死信队列绑定死信交换机的routing key 一致,要不死信队列收不到消息
     * @return
     */
    @Bean("queue_b")
    public Queue queue_b(){
        Map<String, Object> arguments = new HashMap<>();
        // 设置死信交换机
        arguments.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE_Y);
        // 设置死信routing key = dead
        arguments.put("x-dead-letter-routing-key","dead");
        // 设置TTL过期时间
        arguments.put("x-message-ttl",20000);
        return QueueBuilder.nonDurable(QUEUE_B).withArguments(arguments).build();
    }

    /**
     * 声明 死信队列 queue_d
     * @return
     */
    @Bean("queue_d")
    public Queue queue_d(){
        return QueueBuilder.nonDurable(DEAD_LETTER_QUEUE_D).build();
    }

第四步:绑定队列交与换机

    /**
     * 延迟队列(queue_a)绑定(xa)正常交换机(exchange_X)
     * @param queue_a
     * @param exchange_X
     * @return
     */
    @Bean
    public Binding queueABindingExchangeX(@Qualifier("queue_a") Queue queue_a,
                                          @Qualifier("exchange_X") DirectExchange exchange_X){
        return BindingBuilder.bind(queue_a).to(exchange_X).with("xa");
    }

    /**
     * 延迟队列(queue_b)绑定(xb)正常交换机(exchange_X)
     * @param queue_b
     * @param exchange_X
     * @return
     */
    @Bean
    public Binding queueBBindingExchangeX(@Qualifier("queue_b") Queue queue_b,
                                          @Qualifier("exchange_X") DirectExchange exchange_X){
        return BindingBuilder.bind(queue_b).to(exchange_X).with("xb");
    }

    /**
     * 死信队列(queue_d)绑定(dead)死信交换机(exchange_Y)
     * routing key 要和 x-dead-letter-routing-key一致,要不死信队列收不到消息
     * @param queue_d
     * @param exchange_Y
     * @return
     */
    @Bean
    public Binding queueDBindingExchangeY(@Qualifier("queue_d") Queue queue_d,
                                          @Qualifier("exchange_Y") DirectExchange exchange_Y){
        return BindingBuilder.bind(queue_d).to(exchange_Y).with("dead");
    }

}

第五步:编写生产者(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("/mq")
public class RabbitmqController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/producer01/{message}")
    public String producer01(@PathVariable String message) {
        /**
         * 第一个参数:交换机名称
         * 第二个参数:routing key
         * 第三个参数:消息
         */
        rabbitTemplate.convertAndSend("exchange_X","xa","延迟10s的消息:"+message);
        rabbitTemplate.convertAndSend("exchange_X","xb","延迟20s的消息:"+message);
        log.info("【当前时间】:{},发送消息{}给两个TTL队列",new Date().toString(),message);
        return "消息【"+message+"】发送到【queue_a】队列成功";
    }
}

第六步:编写消费者(监听器)

不要导错包了哈

import com.rabbitmq.client.Channel;
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.util.Date;
@Slf4j
@Component
public class DeadLetterQueueCustomer {
    // 监听器
    @RabbitListener(queues = "queue_d")
    public void receiveD(Message message, Channel channel){
        byte[] body = message.getBody();
        String msg = new String(body);
        log.info("【当前时间】:{},接收的消息为 {}",new Date().toString(),msg);
    }
}

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

2、实现【十、2】中的延迟队列(设置每个消息的延迟发送时间)

==rabbitmq中的消息时排队的,这样会导致一个问题,假如:第一秒向队列中发送了一个过期时间为20s的消息A,第2秒向对列中发送了一个过期时间为5s的消息B,10s后B仍然会在队列中,因为B前面有一个A,且A的过期时间大于B,所以B需要等待A过期。
在这里插入图片描述

为例避免上述的情况我们需要在rabbitmq中安装 rabbitmq_delayed_message_exchange-3.9.0 j解决上述的问题(安装如下)。

原理:其原理就是在发送消息时为每条消息设定延迟等待发送时间,等到延迟时间了,那么再将消息通过exchange进行转发,这样对消到达队列的消息为无延迟的消息。消息的延迟效果是在消息发送前的等待实现的。

下载插件并在docker容器中安装插件

下载插件的地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.9.0
在这里插入图片描述

第一步: 下载好插件,并使用sftp工具传到linux中,然后通过 docker cp /home/plugins/rabbitmq_delayed_message_exchange-3.9.0.ez myrabbit:/plugins命令将插件拷贝到docker容器(rabbitmq的plugins目录下)中
在这里插入图片描述
第二步: 进入容器docker exec -it myrabbit /bin/bash,并切换到plugins目录下
在这里插入图片描述
在这里插入图片描述

第三步: 安装插件 rabbitmq-plugins enable rabbitmq_delayed_message_exchange

chmod 777 /plugins/rabbitmq_delayed_message_exchange-3.9.0.ez
在这里插入图片描述

第四步: 使用exit退出容器,然后重启容器docker restart myrabbit
在这里插入图片描述


模型

在生产者发送消息时,设置一个延迟发送时间,当时间到了之后,交换机(exchange_X)转发消息到队列(queue_c),消费者顺势消费消息。
在这里插入图片描述

创建交换机

import org.springframework.amqp.core.*;
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;

/**
 * @author HuXuehao (StudiousTiger)
 * @desc 这个类的作用是编写基于插件的延迟队列
 * @date 2021/10/28
 */
@Configuration
public class rabbitmqConfig {
    // 声明交换机名
    public static final String EXCHANGE_X = "exchange_X";
    // 声明队列名
    public static final String QUEUE_C = "queue_c";
    /**
     * 声明 交换机 exchange_X ,CustomExchange 表示自定义类型
     * 第一个参数:交换机名
     * 第二个参数:交换机类型
     * 第三个参数:false表示不持久化
     * 第四个参数:false表示不自动删除
     * @return
     */
    @Bean("exchange_X")
    public CustomExchange exchange_X(){
        Map<String, Object> arguments = new HashMap<>();
        // 延迟的类型是直接类型
        arguments.put("x-delayed-type","direct");

        // 类型是一个延迟消息
        return new CustomExchange(EXCHANGE_X,"x-delayed-message",false,false);
    }

创建队列queue_c

    /**
     * 声明 延迟队列 queue_c
     * @return
     */
    @Bean("queue_c")
    public Queue queue_c(){
        return QueueBuilder.nonDurable(QUEUE_C).build();
    }

队列queue_c绑定exchange_X

       /**
     * 延迟队列(queue_c)绑定(xc)正常交换机(exchange_X)
     * @param queue_c
     * @param exchange_X
     * @return
     */
    @Bean
    public Binding queueCBindingExchangeX(@Qualifier("queue_c") Queue queue_c,
                                          @Qualifier("exchange_X") CustomExchange exchange_X){
        return BindingBuilder.bind(queue_c).to(exchange_X).with("xc").noargs();
    }

编写生产者
我们使用convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor)这个方法进行发送消息,最后一个参数是一个函数式接口,因此我们可以使用拉姆达斯表示式。
在这里插入图片描述

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
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("/mq")
public class RabbitmqController {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @GetMapping("/producer02/{message}/{timeout}")
    public String producer02(@PathVariable String message,@PathVariable int timeout) {
        // 拉姆达斯表达式
        MessagePostProcessor messagePostProcessor = (Message msg)->{
            // 通过msg获取消息属性,然后设置延迟(Delay)时间(ms)
            msg.getMessageProperties().setDelay(timeout*1000);

            // Message postProcessMessage(Message var1)是有返回值的
            return msg;
        };

        /**
         * 第一个参数:交换机名称
         * 第二个参数:routing key
         * 第三个参数:消息
         * 第四个消息:消息后处理器,我们可以使用它设置消息的过期时间
         */
        rabbitTemplate.convertAndSend("exchange_X","xc","延迟"+timeout+"s的消息:"+message,messagePostProcessor);
        log.info("【当前时间】:{},发送消息{}给两个queue_c队列",new Date().toString(),message);

        return "消息【"+message+"】,"+timeout+"s后发送到【queue_c】队列成功";
    }
}

编写消费者

import com.rabbitmq.client.Channel;
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.util.Date;

@Slf4j
@Component
public class DeadLetterQueueCustomer {
    // 监听器
    @RabbitListener(queues = "queue_c")
    public void receiveD(Message message, Channel channel){
        byte[] body = message.getBody();
        String msg = new String(body);
        log.info("【当前时间】:{},接收的消息为 {}",new Date().toString(),msg);
    }

}

分别发出以下请求

localhost:8080/mq/producer02/studious_timeout_20s/20
localhost:8080/mq/producer02/studious_timeout_5s/5
在这里插入图片描述
完美解决!
在这里插入图片描述

十二、常见面试题

1、Rabbitmq 为什么需要信道,为什么不是TCP直接通信?

  1. TCP的创建和销毁,开销大,创建要三次握手,销毁要4次分手。

  2. 如果不用信道,那应用程序就会TCP连接到Rabbit服务器,高峰时每秒成千上万连接就会造成资源的巨大浪费,而且==底层操作系统每秒处理tcp连接数也是有限制的,==必定造成性能瓶颈。

  3. 信道的原理是一条线程一条信道,多条线程多条信道同用一条TCP连接,一条TCP连接可以容纳无限的信道,即使每秒成千上万的请求也不会成为性能瓶颈。

2、queue队列到底在消费者创建还是生产者创建?

  1. 一般建议是在rabbitmq操作面板创建。这是一种稳妥的做法。
  2. 按照常理来说,确实应该消费者这边创建是最好,消息的消费是在消费者这边。这样你承受一个后果,可能我生产在生产消息可能会丢失消息。
  3. 在生产者创建队列也是可以,这样稳妥的方法,消息是不会出现丢失。
  4. 如果你生产者和消费都创建的队列,谁先启动谁先创建,后面启动就覆盖前面的。


✈ ❀ 希望平凡の我,可以给你不凡の体验 ☂ ✿…
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值