IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 万字教程!RabbitMQ从入门到实战 -> 正文阅读

[Java知识库]万字教程!RabbitMQ从入门到实战

大纲

RabbitMQ (1)

什么是消息队列

Message Queue(MQ),消息队列中间件。很多人都说:MQ 通过将消息的发送和接收分离来实现应用程序的异步和解偶,这个给人的直觉是——MQ 是异步的,用来解耦的,但是这个只是 MQ 的效果而不是目的。

MQ 真正的目的是为了通讯,屏蔽底层复杂的通讯协议,定义了一套应用层的、更加简单的通讯协议。一个分布式系统中两个模块之间通讯要么是 HTTP,要么是自己开发的 TCP,但是这两种协议其实都是原始的协议。

为什么消息中间件不直接使用 HTTP 协议?

HTTP 协议很难实现两端通讯——模块 A 可以调用 B,B 也可以主动调用 A,如果要做到这个两端都要背上 WebServer,而且还不支持长连接(HTTP 2.0 的库根本找不到)。TCP 就更加原始了,粘包、心跳、私有的协议,想一想头皮就发麻。

对于一个消息中间件来说,其主要责任就是负责数据传递,存储,分发,高性能和简洁才是我们所追求的,而 HTTP 请求报文头和响应报文头是比较复杂的,包含了Cookie,数据的加密解密,窗台吗,响应码等附加的功能,我们并不需要这么复杂的功能。

同时大部分情况下 HTTP 大部分都是短链接,在实际的交互过程中,一个请求到响应都很有可能会中断,中断以后就不会执行持久化,就会造成请求的丢失。这样就不利于消息中间件的业务场景,因为消息中间件可能是一个长期的获取信息的过程,出现问题和故障要对数据或消息执行持久化等,目的是为了保证消息和数据的高可靠和稳健的运行。

MQ 所要做的就是在这些协议之上构建一个简单的“协议”——生产者/消费者模型。MQ 带给我的“协议”不是具体的通讯协议,而是更高层次通讯模型。它定义了两个对象——发送数据的叫生产者;接收数据的叫消费者, 提供一个 SDK 让我们可以定义自己的生产者和消费者实现消息通讯而无视底层通讯协议。

消息队列的使用场景

异步处理

场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种串行和并行的方式。

1、串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。

image-20211013175320005

2、并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。

与串行的差别是,并行的方式可以提高处理的时间。

假设三个业务节点每个使用 50 毫秒钟,不考虑网络等其他开销,则串行方式的时间是 150 毫秒,并行的时间可能是 100 毫秒。

image-20211013175431273

如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?

引入消息队列,将不是必须的业务逻辑,异步处理。

image-20211013175537849

按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是 50 毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是 50 毫秒。因此架构改变后,系统的吞吐量提高到每秒 20 QPS。比串行提高了 3 倍,比并行提高了2倍。

应用解耦

场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。

传统模式的缺点:

  1. 假如库存系统无法访问,则订单减库存将失败,从而导致订单失败。
  2. 订单系统与库存系统耦合。

如何解决以上问题呢?引入应用消息队列后的方案。

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。

image-20211013175827906

假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。

流量削峰

流量削峰也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。

应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列,可以控制活动的人数,可以缓解短时间内高流量压垮应用

用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面,秒杀业务根据消息队列中的请求信息,再做后续处理。

AMQP和JMS

MQ是消息通信的模型,并非具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。

两者间的区别和联系:

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

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

RabbitMQ简介

RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。

官方地址:http://www.rabbitmq.com

官方教程:http://www.rabbitmq.com/getstarted.html

RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:

  1. 可靠性(Reliability), RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
  2. 灵活的路由(Flexible Routing), 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
  3. 消息集群(Clustering), 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
  4. 高可用(Highly Available Queues), 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
  5. 多种协议(Multi-protocol), RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
  6. 多语言客户端(Many Clients) ,RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
  7. 管理界面(Management UI), RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
  8. 跟踪机制(Tracing) ,如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
  9. 插件机制(Plugin System), RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

RabbitMQ的架构模型

RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。可以把消息传递的过程想象成:当你将一个包裹送到邮局,邮局会暂存并最终将邮件通过邮递员送到收件人的手上,RabbitMQ就好比由邮局、邮箱和邮递员组成的一个系统。从计算机术语层面来说,RabbitMQ 模型更像是一种交换机模型。

image-20211014135734850

Connection

连接,作为客户端(无论是生产者还是消费者),你如果要与 RabbitMQ 通讯的话,你们之间必须创建一条 TCP 连接,当然同时建立连接后,客户端还必须发送一条“问候语”让彼此知道我们都是符合 AMQP 的语言的,比如你跟别人打招呼一般会说“你好!”,你跟国外的美女一般会说“hello!”一样。

你们确认好“语言”之后,就相当于客户端和 RabbitMQ 通过“认证”了。你们之间可以创建一条 AMQP 的信道(Channel)。

Channel

信道,是生产者/消费者与 RabbitMQ 通信的渠道。信道是建立在 TCP 连接上的虚拟连接,什么意思呢?就是说 rabbitmq 在一条 TCP 上建立成百上千个信道来达到多个线程处理,这个 TCP 被多个线程共享,每个线程对应一个信道,信道在 RabbitMQ 都有唯一的 ID ,保证了信道私有性,对应上唯一的线程使用。

为什么不建立多个 TCP 连接呢?

因为对于操纵系统而言,建立和销毁 TCP 是非常昂贵的,系统为每个线程开辟一个 TCP 是非常消耗性能,每秒成百上千的建立销毁 TCP 会严重消耗系统。所以 rabbitmq 选择建立多个信道(建立在 tcp 的虚拟连接)连接到 rabbit 上。

从技术上讲,这被称之为多路复用,对于执行多个任务的多线程或者异步应用程序来说,它非常有用。

Message

消息,包含有效载荷和标签,有效载荷指要传输的数据,标签描述了有效载荷,并且 rabbitmq 用它来决定谁获得消息,消费者只能拿到有效载荷,并不知道生产者是谁。

Producer

生产者,消息的创建者,发送到 rabbitmq。

Consumer

消费者,消息的消费者,连接到 rabbitmq,订阅到队列上,消费消息,持续订阅和单条订阅。

Broker

代理服务,简单来说就是消息队列服务器实体,默认端口5672。

Exchange

交换机,用来接收生产者发送的消息,然后将这些消息根据路由键发送到队列,主要有四种,后续会介绍。

Routing key

路由规则,虚拟机用它来确认如何路由一个特定消息,即 Exchange 根据这个关键字进行消息投递。

Binding

Exchange 和 Queue 之间的虚拟连接,它的作用就是把exchange和queue按照路由规则绑定起来,Binding 中可以包括多个 Routing key。

Queue

消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。

交换机、队列、绑定、路由键之间的关系

队列通过路由键绑定到交换机,生产者将消息发布到交换机,交换机根据绑定的路由键将消息路由到特定队列,然后由订阅这个队列的消费者进行接收。

image-20211014110649700

Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / ,通过缺省用户和口令 guest 进行访问。

image-20211014111421945

交换机类型

共有四种 direct,fanout,topic,headers,其种 headers(几乎和 direct 一样)不实用,可以忽略。

direct

路由键完全匹配,消息被投递到对应的队列, direct 交换器是默认交换器。ExChange 会将消息发送完全匹配 ROUTING_KEY 的 Queue。

fanout

消息广播到绑定的队列,不管队列绑定了什么路由键,消息经过交换机,每个队列都有一份。

topic

通过使用 **通配符进行处理,使来自不同源头的消息到达同一个队列,通过 . 将路由键分为了几个标识符,*匹配其后面的 1 个标识符,#匹配一个或多个标识符。如:

user.#  # 可以匹配到 user.add  user.add.batch
user.*  # 只能匹配到 user.add ,不能匹配到 user.add.batch

RabbitMQ安装

Windows安装

参考:windows10环境下的RabbitMQ安装步骤

Linux安装

虚拟机,操作系统版本为CentOS7,纯净未安装过RabbitMQ。

百度网盘(其中包括 VMware 和 centos7,提取码:iew6,安装可参考:安装教程)。

配置信息如下:内存为 2G,处理器数量为 2 个

image-20210908135548023

1、Erlang

RabbitMQ服务端代码是使用并发式语言Erlang编写的,安装Rabbit MQ的前提是安装Erlang。

下载地址:https://github.com/rabbitmq/erlang-rpm/releases/

百度网盘:v23.3.el7 (提取码:3jxn)
2、rabbitmq-server

下载地址:https://github.com/rabbitmq/rabbitmq-server/releases/

百度网盘:v3.9.7.el7(提取码:9cxx)

3、安装

在 /usr/local 下创建 rabbitmq 文件夹并放入上面 2 个 rpm 文件(或者直接 wegt 下载),然后执行 yum 安装

yum install ./erlang-23.3.4.7-1.el7.x86_64.rpm
yum install ./rabbitmq-server-3.9.7-1.el7.noarch.rpm

4、启动停止服务

启动服务

service rabbitmq-server start

停止服务

service rabbitmq-server stop

重启服务

service rabbitmq-server restart

查看状态

service rabbitmq-server status

5、安装管理控制台

rabbitmq-plugins enable rabbitmq_managerment

6、访问

ip:15762

注意如果不能访问需要开启端口

# 开启相对应的端口
firewall-cmd --permanent --add-port=15672/tcp
firewall-cmd --permanent --add-port=5672/tcp

image-20211014163005620

Global counts

  • Connections:连接数
  • Channels:频道数
  • Exchanges:交换机数
  • Queues:队列数
  • Consumers:消费者数

交换机页面

image-20211014163415882

队列页面

image-20211014180620916

  • Name:消息队列的名称,这里是通过程序创建的
  • Features:消息队列的类型,durable:true 为会持久化消息
  • Ready:准备好的消息
  • Unacked:未确认的消息
  • Total:全部消息

7、增加名为admin,密码为admin的用户并配置administrator角色,增加相应的权限

#创建用户
rabbitmqctl add_user admin admin
#赋予权限
rabbitmqctl set_user_tags admin administrator
rabbitmqctl set_permissions -p/admin ".*" ".*" ".*"

系统默认的 guest 用户是不能进行远程登录的,除非另行配置相关参数。

image-20211014153634780

8、查看已有虚拟主机并增加虚拟主机

查看已有虚拟主机

rabbitmqctl list_vhosts

添加名为 order 的虚拟主机,如有需要

rabbitmqctl add_vhost order 

9、日志

Linux

/var/log/rabbitmq/rabbit@XXX.log 
/var/log/rabbitmq/rabbit@XXX-sasl.log 

Windows

C:\Users\Administrator\AppData\Roaming\RabbitMQ\log\rabbit@XXX.log 
C:\Users\Administrator\AppData\Roaming\RabbitMQ\log\rabbit@ XXX-sasl.log 

第一个是记录 MQ 启动、连接日志,第二个是 saal 用来记录 Erlang 相关的信息,例如查看 Erlang 崩溃的报告。

消息确认机制ACK

对于消费者,就涉及到消息的确认

消费者收到的每一条消息都必须进行确认(自动确认和自行确认)。

消费者在声明队列时,可以指定 autoAck 参数,当 autoAck = false 时,RabbitMQ 会等待消费者显式发回 ack 信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ 会在队列中消息被消费后立即删除它。 即分2种情况:

  • 自动ACK:消息一旦被接收,消费者自动发送ACK
  • 手动ACK:消息接收后,不会发送ACK,需要手动调用

采用消息确认机制后,只要令 autoAck = false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为 RabbitMQ 会一直持有消息直到消费者显式调用单条订阅(basicAck )为止。当 autoAck = false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息,一部分是已经投递给消费者,但是还没有收到消费者 ack 信号的消息。如果服务器端一直没有收到消费者的 ack 信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。

假设启动两个消费者 A、B,都可以收到消息,但是其中有一个消费者 A 不会对消息进行确认,当把这个消费者 A 关闭后,消费者 B 又会收到本来发送给消费者 A 的消息。所以我们一般使用手动确认的方法是,将消息的处理放在 try/catch 语句块中,成功处理了,就给 RabbitMQ 一个确认应答,如果处理异常了,就在 catch 中,进行消息的拒绝(下文会讲)。

RabbitMQ 不会为未 ack 的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这么设计的原因是 RabbitMQ 允许消费者消费一条消息的时间可以很久很久。

因此,对于如何选择,主要看消息的重要性:

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

常见问题

  1. 如果消息达到无人订阅的队列会怎么办?

    消息会一直在队列中等待,RabbitMq 默认队列是无限长度的。

  2. 多个消费者订阅到同一队列怎么办?

    消息以循环的方式发送给消费者,每个消息只会发送给一个消费者。

  3. 消息路由到了不存在的队列怎么办?

    一般情况下,RabbitMq 会忽略,当这个消息不存在,也就是这消息丢了。

对于上述问题,都在后面的实例中进行验证。

RabbitMQ消息模型

RabbitMQ 提供了 6 种消息模型,但常用的是前面 5 种,第 6 种实际上为RPC,所以一般来说了解前面 5 种即可,而对于后面三种,是根据 Exchange 类型划分的。

image-20211014164714641注:对下面模式的讲解主要基于Java原生API操作,因此在项目中需要添加如下依赖。

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

为了后续的操作先定义一个连接 rabbitmq 的连接工具类

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

public class RabbitMQUtils {

    private static ConnectionFactory connectionFactory;

    static {
        connectionFactory = new ConnectionFactory();
        //我们把重量级资源通过单例模式加载
        connectionFactory.setHost("192.168.153.128");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        //上面创建的VHost
        connectionFactory.setVirtualHost("/order");
    }

    //定义提供连接对象的方法
    public static Connection getConnection() {
        try {
            return connectionFactory.newConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //定义关闭通道和关闭连接工具方法
    public static void closeConnectionAndChanel(Channel channel, Connection conn) {
        try {
            if (channel != null) {
                channel.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

基本消息模型

RabbitMQ 是一个消息代理:它接受和转发消息。可以将其视为邮局:当你将要投递的邮件放入邮箱时,你可以确定信件承运人最终会将邮件递送给你的收件人。在这个比喻中,RabbitMQ 是一个邮箱、一个邮局和一个信件载体。

image-20211014172221536

  • P:生产者,发送消息到消息队列
  • C:消费者:消息的接受者,会一直等待消息到来。
  • queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。

1、发送消息

在原生JavaAPI中,通过queueDeclare方法去申明队列:

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;

参数说明:

  • queue,队列名称。
  • durable,是否持久化,如果持久化,mq重启后队列还在。
  • exclusive,是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建。
  • autoDelete,自动删除,队列不再使用时是否自动删除此队列,如果将此参数和 exclusive 参数设置为 true 就可以实现临时队列(队列不用了就自动删除)。
  • arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间等。

主要通过basicPublish方法

void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;

参数说明:

  • exchange,交换机,如果不指定将使用 mq 的默认交换机(设置为"")。
  • routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称。
  • props,消息的属性。
  • body,消息内容。

代码实现

Producer

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

public class Producer {

    //定义队列名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        // 1、获取到连接
        Connection connection = RabbitMQUtils.getConnection();
        // 2、从连接中创建通道,使用通道才能完成消息相关的操作
        Channel channel = connection.createChannel();
        // 3、声明(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 4、消息内容
        String message = "Hello World!";
        // 向指定的队列中发送消息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}

去控制台查看:

image-20211014180620916

2、接收消息

接收消息consumer#handleDelivery方法:

void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body) throws IOException;

Consumer

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class Consumer {
    private final static String QUEUE_NAME = "hello";
 
    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                System.out.println(new String(body));
            }
        };
        //自动ack
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

work消息模型

多个消费者监听同一队列。消费者接收到消息后, 通过线程池异步消费。但是一个消息只能被一个消费者获取。work queue常用于避免消息堆积问题。

image-20211015092518859

  • P:生产者,发布任务。
  • C1:消费者1,领取任务并且完成任务,假设完成速度较慢(模拟耗时)
  • C2:消费者2,领取任务并且完成任务,假设完成速度较快

Producer

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

public class RabbitMQUtils {

    //定义提供连接对象的方法
    public static Connection getConnection() {
        try {
            ConnectionFactory connectionFactory = new ConnectionFactory();
            //我们把重量级资源通过单例模式加载
            connectionFactory.setHost("192.168.153.128");
            connectionFactory.setPort(5672);
            connectionFactory.setUsername("admin");
            connectionFactory.setPassword("admin");
            connectionFactory.setVirtualHost("order");
            return connectionFactory.newConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //定义关闭通道和关闭连接工具方法
    public static void closeConnectionAndChanel(Channel channel, Connection conn) {
        try {
            if (channel != null) {
                channel.close();
            }
            if (conn != null) {
                conn.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Consumer1

import cn.javatv.javaAPI.RabbitMQUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.util.concurrent.TimeUnit;

public class Consumer1 {
    private final static String QUEUE_NAME = "work";

    public static void main(String[] args) throws Exception {
        // 获取到连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                try {
                    //模拟任务耗时
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Consumer1_" +new String(body));
            }
        };
        //自动ack
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

Consumer2

import cn.javatv.javaAPI.RabbitMQUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class Consumer2 {
    private final static String QUEUE_NAME = "work";

    public static void main(String[] args) throws Exception {
        // 获取到连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                System.out.println("Consumer2_" + new String(body));
            }
        };
        //自动ack
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

先启动消费者,在启动生成者,输出如下:

image-20211015100908236

我们发现消费者是按照轮询消费的,但这种消费存在一个问题,假如 Consumer1 处理能力极快,Consumer2 (代码中休眠了 2s)处理能力极慢,这是 Consumer2 会严重拖累整体消费进度,而 Consuemr1 又早早的完成任务而无所事事。

能者多劳

从上面的结果可以看出,任务是平均分配的,也就是说,不管你上个任务是否完成,我继续把后面的任务分发给你,而实际上为了效率,谁消费得越快,谁就得到越多。因此可以通过 BasicQos 方法的参数设为 1,前提是在手动 ack 的情况下才生效,即autoAck = false

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class Consumer2 {
    private final static String QUEUE_NAME = "work";

    public static void main(String[] args) throws Exception {
        // 获取到连接
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //设置消费者同时只能处理一条消息
        channel.basicQos(1);
        //实现消费方法
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    //模拟任务耗时
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Consumer2_" + new String(body));
                //确认消息
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        //手动ack
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

输出结果:

image-20211015103629746

可以看到 Consumer1 消费了19个,Consumer2 才消费 1 个。

Publish/Subscribe-Fanout

一次向多个消费者发送消息,该模式的交换机类型为Fanout,也称为广播。

image-20211015110303941

它具有以下性质:

  • 可以有多个消费者。
  • 每个消费者有自己的queue。
  • 每个队列都要绑定到Exchange。
  • 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
  • 交换机把消息发送给绑定过的所有队列。
  • 队列的消费者都能拿到消息,实现一条消息被多个消费者消费

Producer

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class FanoutProducer {

    public final static String EXCHANGE_NAME = "fanout";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发类型为FANOUT
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        //发送3条消息,且路由键不同
        for (int i = 1; i <= 3; i++) {
            //路由键,循环3次,路由键为routekey1,routekey2,routekey3
            String routekey = "routekey" + i;
            // 发送的消息
            String message = "fanout_" + i;
            /*
             * 参数1:exchange name 交换机
             * 参数2:routing key   路由键
             */
            channel.basicPublish(EXCHANGE_NAME, routekey, null, message.getBytes());
            System.out.println(" [x] Sent '" + routekey + "':'" + message + "'");
        }
        // 关闭
        channel.close();
        connection.close();
    }
}

Consumer1

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;

public class Consumer1 {

    public final static String EXCHANGE_NAME = "fanout";

    public static void main(String[] argv) throws IOException {
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        // 声明一个随机队列
        String queueName = channel.queueDeclare().getQueue();
        /*
         * 队列绑定到交换器上时,是允许绑定多个路由键的,也就是多重绑定
         */
        String[] routekeys = {"routekey1", "routekey2", "routekey3"};
        for (String routekey : routekeys) {
            //绑定
            channel.queueBind(queueName, FanoutProducer.EXCHANGE_NAME, routekey);
        }
        System.out.println("[" + queueName + "]等待消息:");
        // 创建队列消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("接收" + envelope.getRoutingKey() + ":" + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

我们看看 fanout 的定义:

消息广播到绑定的队列,不管队列绑定了什么路由键,消息经过交换机,每个队列都有一份。

换句话说,只要队列和交换机绑定,不在乎路由键是什么都能接收消息。

如绑定一个不存在的路由键:

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:fanout消费者--绑定一个不存在的路由键
 */
public class Consumer2 {

    public final static String EXCHANGE_NAME = "fanout";

    public static void main(String[] argv) throws IOException, TimeoutException {
        Connection connection = RabbitMQUtils.getConnection();
        final Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
        // 声明一个随机队列
        String queueName = channel.queueDeclare().getQueue();
        //设置一个不存在的路由键
        String routekey = "xxx";
        channel.queueBind(queueName, FanoutProducer.EXCHANGE_NAME, routekey);
        System.out.println("队列[" + queueName + "]等待消息:");

        // 创建队列消费者
        final Consumer consumerB = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                //记录日志到文件:
                System.out.println("接收消息 [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumerB);
    }
}

输出:

队列[amq.gen-G2LL566wrSH3mGBUF6XKCQ]等待消息:
接收消息 [routekey1] fanout_1
接收消息 [routekey2] fanout_2
接收消息 [routekey3] fanout_3

不管我们如何调整生产者和消费者的路由键,都对消息的接收没有影响。

Routing-Direct

在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key),消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

image-20211015140048067

  • P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。

  • X:Exchange,接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列。

  • C1:消费者,其所在队列指定了需要routing key 为 error 的消息。

  • C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息。

Producer

发送 3 种不同类型的日志。

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class DirectProducer {

    public final static String EXCHANGE_NAME = "direct";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接、连接到RabbitMQ
        Connection connection = RabbitMQUtils.getConnection();
        //创建信道
        Channel channel = connection.createChannel();
        //在信道中设置交换器
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //申明队列(放在消费者中去做)
        String[] routeKeys = {"info", "warning", "error"};
        for (int i = 1; i <= 6; i++) {
            String routeKey = routeKeys[i % 3];
            String msg = routeKey + "日志";
            //发布消息
            channel.basicPublish(EXCHANGE_NAME, routeKey, null, msg.getBytes());
            System.out.println("Sent:" + msg);
        }
        channel.close();
        connection.close();
    }
}

生产消息:

Sent:warning日志
Sent:error日志
Sent:info日志
Sent:warning日志
Sent:error日志
Sent:info日志

Consumer1

指定需要routing key 为 error 的消息。

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer1 {

    public final static String EXCHANGE_NAME = "direct";

    public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
        //创建连接、连接到RabbitMQ
        Connection connection = RabbitMQUtils.getConnection();
        //创建一个信道
        final Channel channel = connection.createChannel();
        //信道设置交换器类型(direct)
        channel.exchangeDeclare(DirectProducer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声明一个随机队列
        String queueName = channel.queueDeclare().getQueue();
        //绑定
        channel.queueBind(queueName, DirectProducer.EXCHANGE_NAME, "error");
        System.out.println("队列[" + queueName + "]等待消息:");
        // 创建队列消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("接收消息 [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

接收消息:

队列[amq.gen-NhIiesUDi547ZGr4JBEsnA]等待消息:
接收消息 [error] error日志
接收消息 [error] error日志

Consumer1

指定需要routing key 为 info、error、warning 的消息。

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class Consumer2 {

    public final static String EXCHANGE_NAME = "direct";

    public static void main(String[] args) throws IOException {
        //创建连接、连接到RabbitMQ
        Connection connection = RabbitMQUtils.getConnection();
        //创建一个信道
        final Channel channel = connection.createChannel();
        //信道设置交换器类型(direct)
        channel.exchangeDeclare(DirectProducer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //声明一个随机队列
        String queueName = channel.queueDeclare().getQueue();
        //绑定
        String[] routeKeys = {"info", "warning", "error"};
        for (String routekey : routeKeys) {
            channel.queueBind(queueName, DirectProducer.EXCHANGE_NAME, routekey);
        }
        System.out.println("队列[" + queueName + "]等待消息:");
        // 创建队列消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("接收消息 [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

接收消息:

队列[amq.gen-thfvXuQSfXHEVFRwHKZAFA]等待消息:
接收消息 [warning] warning日志
接收消息 [error] error日志
接收消息 [info] info日志
接收消息 [warning] warning日志
接收消息 [error] error日志
接收消息 [info] info日志

Topics-topic

Topic 类型的 Exchange 与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型 Exchange 可以让队列在绑定 Routing key 的时候使用通配符!

  • # :匹配一个或多个词

  • ***** :匹配一个词

user.#  # 可以匹配到 user.add  user.add.batch
user.*  # 只能匹配到 user.add ,不能匹配到 user.add.batch

假如你准备去买宠物,宠物的种类有 rabbit,cat,dog,宠物的颜色有 white,blue,grey,宠物的性格为 A,B,C。若按照路由键规则:种类 . 颜色 . 性格,则会产生3*3*3=27条消息,如rabbit.white.A

Producer

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class TopicProducer {

    public final static String EXCHANGE_NAME = "topic";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建连接、连接到RabbitMQ
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //宠物种类
        String[] pets = {"rabbit", "cat", "dog"};
        for (int i = 0; i < 3; i++) {
            //宠物颜色
            String[] colors = {"white", "blue", "grey"};
            for (int j = 0; j < 3; j++) {
                //宠物性格
                String[] character = {"A", "B", "C"};
                for (int k = 0; k < 3; k++) {
                    // 发送的消息
                    String routeKey = pets[i % 3] + "." + colors[j % 3] + "." + character[k % 3];
                    String message = "宠物信息:" + routeKey;
                    channel.basicPublish(EXCHANGE_NAME, routeKey, null, message.getBytes());
                    System.out.println(" [x] Sent " + message);
                }
            }
        }
        // 关闭连接
        channel.close();
        connection.close();
    }
}

Consumer

1、如果你是开宠物店,需要所有的宠物

routingKey = #

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;

public class Consumer {

    public static void main(String[] argv) throws IOException {
        //创建连接、连接到RabbitMQ
        Connection connection = RabbitMQUtils.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(TopicProducer.EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //声明一个随机队列
        String queueName = channel.queueDeclare().getQueue();
        //routingKey设置为 #
        channel.queueBind(queueName, TopicProducer.EXCHANGE_NAME, "#");
        System.out.println("队列[" + queueName + "]等待消息:");
        // 创建队列消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("接收消息 [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

接收消息:

队列[amq.gen-eaK9M1vqEtY6WjivxrzqfA]等待消息:
接收消息 [rabbit.white.A] 宠物信息:rabbit.white.A
接收消息 [rabbit.white.B] 宠物信息:rabbit.white.B
接收消息 [rabbit.white.C] 宠物信息:rabbit.white.C
接收消息 [rabbit.blue.A] 宠物信息:rabbit.blue.A
接收消息 [rabbit.blue.B] 宠物信息:rabbit.blue.B
......
//接收所有消息,省略

2、如果你仅仅是想买猫,但是想先了解猫的颜色和性格

消费者代码同上,修改channel.queueBind(queueName,TopicProducer.EXCHANGE_NAME,"cat.#")即可

routingKey = cat.#

接收消息

队列[amq.gen-Fy0aH4610sLNLrkoJKl_uA]等待消息:
接收消息 [cat.white.A] 宠物信息:cat.white.A
接收消息 [cat.white.B] 宠物信息:cat.white.B
接收消息 [cat.white.C] 宠物信息:cat.white.C
接收消息 [cat.blue.A] 宠物信息:cat.blue.A
接收消息 [cat.blue.B] 宠物信息:cat.blue.B
接收消息 [cat.blue.C] 宠物信息:cat.blue.C
接收消息 [cat.grey.A] 宠物信息:cat.grey.A
接收消息 [cat.grey.B] 宠物信息:cat.grey.B
接收消息 [cat.grey.C] 宠物信息:cat.grey.C

3、如果你想买 A 种性格的猫

routingKey = cat.*.A 或 routingKey = cat.#.A

接收消息:

队列[amq.gen-xSuwMezB1VcEhcR0SfeKGA]等待消息:
接收消息 [cat.white.A] 宠物信息:cat.white.A
接收消息 [cat.blue.A] 宠物信息:cat.blue.A
接收消息 [cat.grey.A] 宠物信息:cat.grey.A

4、如果你想买白颜色的宠物

routingKey = #.white.#

接收消息:

队列[amq.gen-1HSVv0nTfApQ_PT98lF-qQ]等待消息:
接收消息 [rabbit.white.A] 宠物信息:rabbit.white.A
接收消息 [rabbit.white.B] 宠物信息:rabbit.white.B
接收消息 [rabbit.white.C] 宠物信息:rabbit.white.C
接收消息 [cat.white.A] 宠物信息:cat.white.A
接收消息 [cat.white.B] 宠物信息:cat.white.B
接收消息 [cat.white.C] 宠物信息:cat.white.C
接收消息 [dog.white.A] 宠物信息:dog.white.A
接收消息 [dog.white.B] 宠物信息:dog.white.B
接收消息 [dog.white.C] 宠物信息:dog.white.C

5、如果你想买 B 种性格的宠物

routingKey = #.B

接收消息:

队列[amq.gen-K-XtEdYjBHwcx6nAuUwhBg]等待消息:
接收消息 [rabbit.white.B] 宠物信息:rabbit.white.B
接收消息 [rabbit.blue.B] 宠物信息:rabbit.blue.B
接收消息 [rabbit.grey.B] 宠物信息:rabbit.grey.B
接收消息 [cat.white.B] 宠物信息:cat.white.B
接收消息 [cat.blue.B] 宠物信息:cat.blue.B
接收消息 [cat.grey.B] 宠物信息:cat.grey.B
接收消息 [dog.white.B] 宠物信息:dog.white.B
接收消息 [dog.blue.B] 宠物信息:dog.blue.B
接收消息 [dog.grey.B] 宠物信息:dog.grey.B

6、如果你想买白色,C种性格的猫

routingKey = cat.white.C

接收消息:

队列[amq.gen-LojPv9XhqR_y5SE0wqeduA]等待消息:
接收消息 [cat.white.C] 宠物信息:cat.white.C

RabbitMQ进阶

在 RabbitMQ 在设计的时候,特意让生产者和消费者“脱钩”,也就是消息的发布和消息的消费之间是解耦的。

在 RabbitMQ 中,有不同的投递机制(生产者),但是每一种机制都对性能有一定的影响。一般来讲速度快的可靠性低,可靠性好的性能差,具体怎么使用需要根据你的应用程序来定,所以说没有最好的方式,只有最合适的方式。只有把你的项目和技术相结合,才能找到适合你的平衡。

消息发布的权衡

不做任何配置的情况下,生产者是不知道消息是否真正到达RabbitMQ,也就是说消息发布操作不返回任何消息给生产者。怎么保证我们消息发布的可靠性投递?有以下几种常用机制。

image-20211015164824532

在 RabbitMQ 中实际项目中,生产者和消费者都是客户端,它们都可以完成申明交换器、申明队列和绑定关系,但是在我们的实战过程中,我们在生产者代码中申明交换器,在消费者代码中申明队列和绑定关系。

另外,生产者发布消息时不一定非得需要消费者,对于 RabbitMQ 来说,如果是单纯的生产者你只需要生产者客户端、申明交换器、申明队列、确定绑定关系,数据就能从生产者发送至 RabbitMQ。而在面的例子中,为了演示的方便,基本都是先使用消费者消费队列中的数据来方便展示结果。

无保障

上面演示消息模型中使用的就是无保障的方式,通过 basicPublish 发布消息并使用正确的交换器和路由信息,消息会被接收并发送到合适的队列中。但是如果有网络问题,或者消息不可路由,或者 RabbitMQ 自身有问题的话,这种方式就有风险。所以无保证的消息发送一般情况下不推荐。

如在上面测试中可以发现当你生产多条消息,经过指定的路由之后,消费者只会得到需要的那部分数据,其他数据则丢失。

失败通知

生产者发送消息时设置 mandatory 标志,如果消息不可路由,将消息返回给发送者,并通知失败。

注意:它只会让 RabbitMQ 向你通知失败,而不会通知成功。如果消息正确路由到队列,则发布者不会受到任何通知。带来的问题是无法确保发布消息一定是成功的,因为通知失败的消息可能会丢失。

即失败通知是如果这条消息没有被投递进队列,或者在队列里消费失败了就会触发失败通知,失败通知的对象是队列,只跟有没有被队列正确的消费有关。

image-20211018144006239

Producer

Channel#basicPublish方法的mandatory 设置为 true ,而该方法是一个 void 方法,因此我们需要通过Channel#addReturnListener方法回调,代码如下:

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ReturnListener;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:生产者——失败确认模式
 */
public class ProducerMandatory {

    public final static String EXCHANGE_NAME = "mandatory_test";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定Direct交换器
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        //失败通知 回调
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replycode, String replyText, String exchange, String routeKey, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
                String message = new String(bytes);
                System.out.println("返回的message:" + message);
                System.out.println("返回的replycode:" + replycode);
                System.out.println("返回的replyText:" + replyText);
                System.out.println("返回的exchange:" + exchange);
                System.out.println("返回的routeKey:" + routeKey);
            }
        });

        String[] routekeys = {"rabbit", "cat", "dog"};
        for (int i = 0; i < 3; i++) {
            String routekey = routekeys[i % 3];
            // 发送的消息
            String message = "Hello World_" + (i + 1) + ("_" + System.currentTimeMillis());
            channel.basicPublish(EXCHANGE_NAME, routekey, true, null, message.getBytes());
            System.out.println("----------------------------------");
            System.out.println("Sent Message: [" + routekey + "]:'" + message + "'");
            Thread.sleep(200);
        }

        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

Consumer

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;

/**
 * 类说明:消费者——失败确认模式(消费者只绑定了cat)
 */
public class ConsumerProducerMandatory {

    public static void main(String[] argv) throws IOException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(ProducerMandatory.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

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

        String routekey = "cat";
        channel.queueBind(queueName, ProducerMandatory.EXCHANGE_NAME, routekey);

        System.out.println(" [*] Waiting for messages......");

        // 创建队列消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                //记录日志到文件:
                System.out.println("Received [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

输出:

----------------------------------
 Sent Message: [rabbit]:'Hello World_1_1634537990867'
返回的message:Hello World_1_1634537990867
返回的replycode:312
返回的replyText:NO_ROUTE
返回的exchange:mandatory_test
返回的routeKey:rabbit
----------------------------------
 Sent Message: [cat]:'Hello World_2_1634537991079'
----------------------------------
 Sent Message: [dog]:'Hello World_3_1634537991284'
返回的message:Hello World_3_1634537991284
返回的replycode:312
返回的replyText:NO_ROUTE
返回的exchange:mandatory_test
返回的routeKey:dog

事务

事务的实现主要是对信道(Channel)的设置,主要的方法有三个:

  1. channel.txSelect()声明启动事务模式
  2. channel.txComment()提交事务
  3. channel.txRollback()回滚事务

在发送消息之前,需要声明 channel 为事务模式,提交或者回滚事务即可。

开启事务后,客户端和 RabbitMQ 之间的通讯交互流程:

  1. 客户端发送给服务器 Tx.Select(开启事务模式)
  2. 服务器端返回 Tx.Select-Ok(开启事务模式 ok)
  3. 推送消息
  4. 客户端发送给事务提交 Tx.Commit
  5. 服务器端返回 Tx.Commit-Ok

以上就完成了事务的交互流程,如果其中任意一个环节出现问题,就会抛出 IoException,这样用户就可以拦截异常进行事务回滚,或决定要不要重复消息。

Producer

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 *类说明:生产者——事务模式
 */
public class ProducerTransaction {

    public final static String EXCHANGE_NAME = "producer_transaction";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        String[] routekeys={"rabbit","cat","dog"};
        //加入事务
        channel.txSelect();
        try {
            for(int i=0;i<3;i++){
                String routekey = routekeys[i%3];
                // 发送的消息
                String message = "Hello World_"+(i+1) +("_"+System.currentTimeMillis());
                channel.basicPublish(EXCHANGE_NAME, routekey, true, null, message.getBytes());
                System.out.println("----------------------------------");
                System.out.println(" Sent Message: [" + routekey +"]:'" + message + "'");
                Thread.sleep(200);
            }
            //事务提交
            channel.txCommit();
        } catch (IOException e) {
            e.printStackTrace();
            //事务回滚
            channel.txRollback();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

Consumer

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:消费者——事务模式
 */
public class ConsumerProducerTransaction {

    public static void main(String[] argv) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(ProducerTransaction.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        String queueName = "producer_confirm";
        channel.queueDeclare(queueName, false, false, false, null);

        String routekey = "cat";
        channel.queueBind(queueName, ProducerTransaction.EXCHANGE_NAME, routekey);

        System.out.println(" [*] Waiting for messages......");

        // 创建队列消费者
        final Consumer consumerB = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                //记录日志到文件:
                System.out.println("Received [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumerB);
    }
}

需要注意的是,事务的性能是非常差的。根据相关资料,事务会降低2~10倍的性能,而且使用消息中间件的目的就是业务解耦和异步处理,使用事务就打破了这个条件,因为事务是同步的,所以一般情况下不推荐使用事务方式。

发送方确认模式

基于事务的性能问题,RabbitMQ团队为我们拿出了更好的方案,即采用发送方确认模式,该模式比事务更轻量,性能影响几乎可以忽略不计。

原理:生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),由这个 id 在生产者和 RabbitMQ 之间进行消息的确认。

  • 不可路由的消息,当交换器发现,消息不能路由到任何队列,会进行确认操作,表示收到了消息。如果发送方设置了 mandatory 模式,则会先调用addReturnListener 监听器。

    image-20211018152143470

  • 可路由的消息,要等到消息被投递到所有匹配的队列之后,broker 会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号。

    image-20211018152205066

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

  • 一般确认模式:每发送一条消息后,调用waitForConfirms()方法,等待服务器端Confirm。实际上是一种串行Confirm了,每publish一条消息之后就等待服务端Confirm,如果服务端返回false或者超时时间内未返回,客户端进行消息重传。
  • 批量确认模式:批量Confirm模式,每发送一批消息之后,调用 waitForConfirms() 方法,等待服务端Confirm,这种批量确认的模式极大的提高了 Confirm 效率,但是如果一旦出现 Confirm 返回false或者超时的情况,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息,如果这种情况频繁发生的话,效率也会不升反降。
  • 异步确认模式:提供一个回调方法,服务端 Confirm了一条或者多条消息后 Client 端会回调这个方法。

Consumer

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:消费者——发送方确认模式
 */
public class ConfirmConsumer {

    //对应3种不同的模式:confirm,producer_wait_confirm,producer_async_confirm
    public final static String EXCHANGE_NAME = "producer_async_confirm";

    public static void main(String[] argv) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = EXCHANGE_NAME;
        channel.queueDeclare(queueName, false, false, false, null);

        String routekey = "cat";
        channel.queueBind(queueName, EXCHANGE_NAME, routekey);

        System.out.println(" [*] Waiting for messages......");

        // 创建队列消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                //记录日志到文件:
                System.out.println("Received [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

1、一般确认模式

Channel#waitForConfirms(),一般发送方确认模式,消息到达交换器,就会返回 true。

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 *类说明:生产者——发送方确认模式--一般确认
 */
@Slf4j(topic = "mq")
public class ProducerConfirm {

    public final static String EXCHANGE_NAME = "confirm";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 启用发送者确认模式
        channel.confirmSelect();
        String routekey = "cat";
        for (int i = 0; i < 2; i++) {
            // 发送的消息
            String message = "Hello World_" + (i + 1);
            //参数1:exchange name
            //参数2:routing key
            channel.basicPublish(EXCHANGE_NAME, routekey, true, null, message.getBytes());
            log.info("Sent Message: [" + routekey + "]:'" + message + "'");
            //确认是否成功(true成功)
            if (channel.waitForConfirms()) {
                log.info("send success");
            } else {
                log.info("send failure");
            }
        }
        // 关闭信道和连接
//        channel.close();
//        connection.close();
    }
}

输出:

16:52:56.061 [main] INFO  mq - Sent Message: [cat]:'Hello World_1'
16:52:56.063 [main] INFO  mq - send success
16:52:56.063 [main] INFO  mq - Sent Message: [cat]:'Hello World_2'
16:52:56.064 [main] INFO  mq - send success

//可以看出是发送一条消息,等待服务器确认后在发送第二条消息

2、批量确认模式

Channel#waitForConfirms(),使用同步方式等所有的消息发送之后才会执行后面代码,只要有一个消息未到达交换器就会抛出 IOException 异常。

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 *类说明:生产者——发送方确认模式--批量确认
 */
public class ProducerBatchConfirm {

    public final static String EXCHANGE_NAME = "producer_wait_confirm";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 启用发送者确认模式
        channel.confirmSelect();
        String routekey = "cat";
        for(int i=0;i<10;i++){
            // 发送的消息
            String message = "Hello World_"+(i+1);
            //参数1:exchange name
            //参数2:routing key
            channel.basicPublish(EXCHANGE_NAME, routekey, true,null, message.getBytes());
            System.out.println(" Sent Message: [" + routekey +"]:'"+ message + "'");
        }
        // 启用发送者确认模式(批量确认)
        channel.waitForConfirmsOrDie();
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

3、异步监听模式

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import com.rabbitmq.client.ReturnListener;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:生产者——发送方确认模式--异步监听确认
 */
public class ProducerConfirmAsync {

    public final static String EXCHANGE_NAME = "producer_async_confirm";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 启用发送者确认模式
        channel.confirmSelect();
        // 添加发送者确认监听器
        channel.addConfirmListener(new ConfirmListener() {
            //成功,发送一定消息数量之后 multiple = true 即会转为批量操作
            public void handleAck(long deliveryTag, boolean multiple) {
                System.out.println("send_ACK:" + deliveryTag + ",multiple:" + multiple);
            }

            //失败
            public void handleNack(long deliveryTag, boolean multiple) {
                System.out.println("Erro----send_NACK:" + deliveryTag + ",multiple:" + multiple);
            }
        });

        // 添加失败者通知
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replyCode, String replyText,
                                     String exchange, String routingKey,
                                     AMQP.BasicProperties properties,
                                     byte[] body)
                    throws IOException {
                String message = new String(body);
                System.out.println("RabbitMq路由失败:  " + routingKey + "." + message);
            }
        });
        String[] routekeys = {"cat", "dog"};
        for (int i = 0; i < 20; i++) {
            String routekey = routekeys[i % 2];
            // 发送的消息
            String message = "Hello World_" + (i + 1) + ("_" + System.currentTimeMillis());
            channel.basicPublish(EXCHANGE_NAME, routekey, true, MessageProperties.PERSISTENT_BASIC, message.getBytes());
        }
        // 关闭频道和连接,如果要看回调需要注释
        //channel.close();
        //connection.close();
    }
}

输出:

RabbitMq路由失败:  dog.Hello World_2_1634718841248
send_ACK:2,multiple:false
RabbitMq路由失败:  dog.Hello World_4_1634718841248
send_ACK:4,multiple:false
RabbitMq路由失败:  dog.Hello World_6_1634718841248
send_ACK:6,multiple:false
RabbitMq路由失败:  dog.Hello World_8_1634718841248
send_ACK:8,multiple:false
RabbitMq路由失败:  dog.Hello World_10_1634718841249
send_ACK:10,multiple:false
RabbitMq路由失败:  dog.Hello World_12_1634718841249
send_ACK:12,multiple:false
RabbitMq路由失败:  dog.Hello World_14_1634718841249
send_ACK:11,multiple:true
send_ACK:14,multiple:false
RabbitMq路由失败:  dog.Hello World_16_1634718841249
send_ACK:16,multiple:true
RabbitMq路由失败:  dog.Hello World_18_1634718841249
send_ACK:18,multiple:false
RabbitMq路由失败:  dog.Hello World_20_1634718841249
send_ACK:20,multiple:false
send_ACK:19,multiple:true

//对于无法路由的消息回调失败通知

备用交换器

如果主交换器无法路由消息,那么消息将被路由到这个备用的交换器上。

如果发布消息时同时设置了 mandatory 会发生什么?如果主交换器无法路由消息,RabbitMQ 并不会通知发布者,因为,向备用交换器发送消息,表示消息已经被路由了。

注意,新的备用交换器就是普通的交换器,没有任何特殊的地方。

使用备用交换器,向往常一样,声明 Queue 和备用交换器,把 Queue 绑定到备用交换器上。然后在声明主交换器时,通过交换器的参数 alternate-exchange,将备用交换器设置给主交换器。

建议备用交换器设置为 faout 类型,Queue 绑定时的路由键设置为#

Producer

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

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:生产者--绑定了一个备用交换器
 */
public class BackupExProducer {

    public final static String EXCHANGE_NAME = "main-exchange";
    public final static String BAK_EXCHANGE_NAME = "alternate-exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 声明备用交换器
        Map<String, Object> argsMap = new HashMap<String, Object>();
        argsMap.put("alternate-exchange", BAK_EXCHANGE_NAME);
        //主交换器
        channel.exchangeDeclare(EXCHANGE_NAME, "direct", false, false, argsMap);
        //备用交换器
        channel.exchangeDeclare(BAK_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);
        //所有的消息
        String[] routekeys = {"rabbit", "cat", "dog"};
        for (int i = 0; i < 3; i++) {
            //每一次发送一条不同宠物的消息
            String routekey = routekeys[i % 3];
            // 发送的消息
            String message = "Hello World_" + (i + 1);
            //参数1:exchange name
            //参数2:routing key
            channel.basicPublish(EXCHANGE_NAME, routekey, null, message.getBytes());
            System.out.println(" [x] Sent '" + routekey + "':'" + message + "'");
        }
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

Consumer

主交换器

/**
 * 类说明:消费者——一般消费者
 */
public class MainConsumer {


    public static void main(String[] argv) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();

        // 声明一个队列
        String queueName = "backupexchange";
        channel.queueDeclare(queueName, false, false, false, null);
        String routekey = "cat";
        channel.queueBind(queueName, BackupExProducer.EXCHANGE_NAME, routekey);
        System.out.println(" [*] Waiting for messages......");
        // 创建队列消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                //记录日志到文件:
                System.out.println("Received [" + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

备用交换器

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:消费者——绑定备用交换器队列的消费者
 */
public class BackupExConsumer {


    public static void main(String[] argv) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(BackupExProducer.BAK_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);
        // 声明一个队列
        String queueName = "fetchother";
        channel.queueDeclare(queueName, false, false, false, null);

        channel.queueBind(queueName, BackupExProducer.BAK_EXCHANGE_NAME, "#");

        System.out.println(" [*] Waiting for messages......");

        // 创建队列消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body)
                    throws IOException {
                String message = new String(body, "UTF-8");
                //记录日志到文件:
                System.out.println("Received ["
                        + envelope.getRoutingKey() + "] " + message);
            }
        };
        channel.basicConsume(queueName, true, consumer);
    }
}

可以看到输出结果,未被路由的消息,转到了备用交换器队列中了。

image-20211018170245441

小结

生产者消息发布权衡如果想要投递消息越快那么可靠性越低,如果保证可靠性越高,那么速度就会相应的有所减慢。这个需要看具体使用场景来权衡。一般情况下使用失败通知+发布者确认+备用交换器就能完成比较高的可靠性消息投递,并且速度也不会太慢。

消息消费的权衡

消息的消费主要有两种,第一种推送(Consume ),另外一种就是拉取(Get)

推送Consume

在上面的代码中都是用的推送的方式,当注册一个消费者后,RabbitMQ 会在消息可用时,自动将消息进行推送给消费者。

拉取get

拉取属于一种轮询模型,发送一次 get 请求,获得一个消息。如果此时 RabbitMQ 中没有消息,会获得一个表示空的回复(white循环)。总的来说,这种方式性能比较差,很明显,每获得一条消息,都要和 RabbitMQ 进行网络通信发出请求。而且对 RabbitMQ 来说,RabbitMQ 无法进行任何优化,因为它永远不知道应用程序何时会发出请求。

Producer

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:普通生产者
 */
public class GetMessageProducer {

    public final static String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        for (int i = 0; i < 3; i++) {
            // 发送的消息
            String message = "Hello World_" + (i + 1);
            channel.basicPublish(EXCHANGE_NAME, "error", null, message.getBytes());
            System.out.println(" [x] Sent 'error':'" + message + "'");
        }
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

Consumer

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.GetResponse;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:消费者——拉取模式
 */
public class GetMessageConsumer {


    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(GetMessageProducer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 声明一个队列
        String queueName = "focuserror";
        channel.queueDeclare(queueName, false, false, false, null);

        String routekey = "error";//只关注error级别的日志,然后记录到文件中去。
        channel.queueBind(queueName, GetMessageProducer.EXCHANGE_NAME, routekey);

        System.out.println(" [*] Waiting for messages......");
        //无限循环拉取
        while (true) {
            //拉一条,自动确认的(rabbit 认为这条消息消费 -- 从队列中删除)
            GetResponse getResponse = channel.basicGet(queueName, true);
            if (null != getResponse) {
                System.out.println("received["
                        + getResponse.getEnvelope().getRoutingKey() + "]"
                        + new String(getResponse.getBody()));
            }
            //确认(自动、手动)
            channel.basicAck(0, true);
            Thread.sleep(1000);
        }
    }
}

QoS 预取模式

除了上面 2 种方式之外,还有一种高效率的方式,QoS 预取模式

该模式在确认消息被接收之前,消费者可以预先要求接收一定数量的消息,在处理完一定数量的消息后,批量进行确认。如果消费者应用程序在确认消息之前崩溃,则所有未确认的消息将被重新发送给其他消费者。所以这里存在着一定程度上的可靠性风险。

这种机制一方面可以实现限速(将消息暂存到 RabbitMQ 内存中)的作用,一方面可以保证消息确认质量(比如确认了但是处理有异常的情况)。

注意:消费确认模式必须是非自动 ACK 机制(这个是使用 baseQos 的前提条件,否则会 Qos 不生效),然后设置 basicQos 的值。另外,还可以基于consume 和 channel 的粒度进行设置(global)。

Producer

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:发送消息(发送210条消息,其中第210条消息表示本批次消息的结束)
 */
public class QosProducer {

    public final static String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        //发送210条消息,其中第210条消息表示本批次消息的结束
        for (int i = 0; i < 210; i++) {
            // 发送的消息
            String message = "Hello World_" + (i + 1);
            if (i == 209) { //最后一条
                message = "stop";
            }
            channel.basicPublish(EXCHANGE_NAME, "error", null, message.getBytes());
            System.out.println(" [x] Sent 'error':'" + message + "'");
        }
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

Consumer

设置Channel#basicQos(),并且要自动确认消息。

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 *类说明:普通的消费者
 */
public class QosConsumerMain {

    public static void main(String[] argv) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(QosProducer.EXCHANGE_NAME,BuiltinExchangeType.DIRECT);
        String queueName = "focuserror";
        channel.queueDeclare(queueName,false,false, false,null);
        String routekey = "error";
        channel.queueBind(queueName,QosProducer.EXCHANGE_NAME,routekey);
        System.out.println("waiting for message........");
        final Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Received["+envelope.getRoutingKey() +"]"+message);
                //true:单条确认  false:批量确认
                channel.basicAck(envelope.getDeliveryTag(),true);
            }
        };

        //150条预取(150都取出来 150, 210-150  60  )
        channel.basicQos(500,true);
        //消费者正式开始在指定队列上消费消息
        channel.basicConsume(queueName,false,consumer);
    }
}

消费者中的事务

使用方法和生产者一致,假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,会是什么样的结果?结果分为两种情况:

  1. autoAck=false, 手动应对的时候是支持事务的,也就是说即使你已经手动确认了消息已经收到了,但 RabbitMQ 对消息的确认会等事务的返回结果,再做最终决定是确认消息还是重新放回队列,如果你手动确认之后,又回滚了事务,那么以事务回滚为准,此条消息会重新放回队列。
  2. autoAck=true ,如果自动确认为 true 的情况是不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,队列已经把消息移除了。

消息的拒绝

在正常情况下,生产者发送的消息在被消费者消费后是需要确认的,即autoAck=true自动确认,但如果在手动确认的情况下,一旦消息本身或者消息的处理过程出现问题(比如这消息并不是消费者需要的),这个时候就需要一种机制一种机制,通知 RabbitMQ,这个消息,我无法处理,请让别的消费者处理。常见的有 2 种方式,RejectNack

requeue

RejectNack一般配合 requeue使用:

Reject 在拒绝消息时,如果requeue = true,则会告诉 RabbitMQ 是否需要重新发送给别的消费者。如果是 false 则不重新发送,一般这个消息就会被RabbitMQ 丢弃。Reject 一次只能拒绝一条消息。如果是 true 则消息发生了重新投递

Nack 跟 Reject 类似,只是它可以一次性拒绝多个消息。也可以使用 requeue 标识,这是 RabbitMQ 对 AMQP 规范的一个扩展。

举个栗子

requeue = true的情况下,消息队列中有 10 条消息,有三个消费者,有两个消费可以正常消费消息,有一个消费进行消息的拒绝,过程如下:

  1. 三个消费者订阅一个队列,消息使用轮询的方式进行发送。

    image-20211019105105559

  2. 有一个消费者拒绝消息,同时 requeue 参数设置为 true,消息准备进行重新投递。

    image-20211019105558862

  3. 再使用消息轮询的方式,把三条消息方便发送至三个消费者,其中又会发生一次消息拒绝和消息的重新投递。

    image-20211019110209635

注意:在实际代码中可能不是按照顺序消费的,所以不需要纠结消费得顺序。

Producer

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:存放到延迟队列的元素,对业务数据进行了包装
 */
public class RejectProducer {

    public final static String EXCHANGE_NAME = "reject";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        for (int i = 0; i < 10; i++) {
            // 发送的消息
            String message = "Hello World_" + (i + 1);
            channel.basicPublish(EXCHANGE_NAME, "error", null, message.getBytes());
            System.out.println("[x] Sent 'error':'" + message + "'");
        }
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

ConsumerA

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;

/**
 * 类说明:普通的消费者
 */
public class NormalConsumerA {

    public static void main(String[] args) throws IOException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(RejectProducer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = "reject";
        channel.queueDeclare(queueName, false, false, false, null);
        String routekey = "error";
        channel.queueBind(queueName, RejectProducer.EXCHANGE_NAME, routekey);
        System.out.println("waiting for message........");
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Received[" + envelope.getRoutingKey() + "]" + message);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(queueName, false, consumer);
    }
}

ConsumerB

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:普通的消费者
 */
public class NormalConsumerB {

    public static void main(String[] argv) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(RejectProducer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = "reject";
        channel.queueDeclare(queueName, false, false, false, null);
        String routekey = "error";
        channel.queueBind(queueName, RejectProducer.EXCHANGE_NAME, routekey);
        System.out.println("waiting for message........");
        //声明了一个消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                try {
                    String message = new String(body, "UTF-8");
                    System.out.println("Received[" + envelope.getRoutingKey() + "]" + message);
                    channel.basicAck(envelope.getDeliveryTag(), false);
                } catch (Exception e) {
                    channel.basicReject(envelope.getDeliveryTag(), true);
                }
            }
        };
        channel.basicConsume(queueName, false, consumer);
    }
}

Reject

Reject消费者,通过模拟异常从而进行重新投递。

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:Reject消费者
 */
public class RejectRequeuConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(RejectProducer.EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        String queueName = "reject";
        channel.queueDeclare(queueName, false, false, false, null);

        String routekey = "error";
        channel.queueBind(queueName, RejectProducer.EXCHANGE_NAME, routekey);

        System.out.println("waiting for message........");

        /*声明了一个消费者*/
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                try {
                    String message = new String(body, "UTF-8");
                    System.out.println("Reject消费者 Received[" + envelope.getRoutingKey() + "]" + message);
                    //模拟异常
                    throw new RuntimeException("处理异常" + message);
                } catch (Exception e) {
                    e.printStackTrace();
                    //Reject方式拒绝(这里第2个参数决定是否重新投递)
                    channel.basicReject(envelope.getDeliveryTag(),true);
                }
            }
        };
        channel.basicConsume(queueName, false, consumer);
    }
}

启动消费者A,B,和Reject消费者,在启动生产者发送 10 条消息,可以看到Reject消费者收到消息,并重新投递给消费者A,B。

image-20211019112712307

Nack

Nack 和 Reject 类似,只需要把上述代码中的Channel#basicReject()改为Channel#basicNackt()即可。

//Nack方式的拒绝(第2个参数决定是否批量)
channel.basicNack(envelope.getDeliveryTag(), false, true);

死信和死信队列

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

**为什么为有死信?**消息变成死信一般是以下三种情况:

  1. 消息被拒绝,即basicReject/basicNack,并且设置 requeue 参数为 false,这种情况一般消息丢失 。
  2. 消息过期(TTL),TTL全称为Time-To-Live,表示的是消息的有效期,默认情况下 Rabbit 中的消息不过期,但是可以设置队列的过期时间和消息的过期时间以达到消息过期的效果 ,消息如果在队列中一直没有被消费并且存在时间超过了TTL,消息就会变成了"死信" ,后续无法再被消费。
  3. 队列达到最大长度,一般当设置了最大队列长度或大小并达到最大值时。

死信交换器DLX

在消息的拒绝操作都是在requeue = true情形下,如果为 false 可以发现当发生异常确认后,消息丢失了,这肯定是不能容忍的,所以提出了死信交换器(dead-letter-exchange)的概念。

死信交换器仍然只是一个普通的交换器,创建时并没有特别要求和操作。在创建队列的时候,声明该交换器将用作保存被拒绝的消息即可,相关的参数是 x-dead-letter-exchange。当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的 Exchange 上去,进而被路由到另一个队列。

举个栗子

1、生产者生产 3 条消息

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

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class DlxProducer {

    public final static String EXCHANGE_NAME = "dlx_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        // 指定转发
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String[] routekeys = {"rabbit", "cat", "dog"};
        for (int i = 0; i < 3; i++) {
            String routekey = routekeys[i % 3];
            String msg = "Hello,RabbitMq" + (i + 1);
            channel.basicPublish(EXCHANGE_NAME, routekey, null, msg.getBytes());
            System.out.println("Sent " + routekey + ":" + msg);
        }
        // 关闭频道和连接
        channel.close();
        connection.close();
    }
}

2、普通消费者消费消息,但是不能消费全部的消息,并把不能消费得消息投递到死信队列。如果是我们还想做点其他事情,我们可以在死信交换的时候改变死信消息的路由键,具体的相关的参数是 x-dead-letter-routing-key

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:普通的消费者,但是自己无法消费的消息,将投入死信队列
 */
public class NormalDlxConsumer {

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(DlxProducer.EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        //绑定死信交换器
        //声明一个队列,并绑定死信交换器
        String queueName = "dlx_queue";
        Map<String, Object> argos = new HashMap<String, Object>();
        argos.put("x-dead-letter-exchange", DlxConsumer.DLX_EXCHANGE_NAME);
        //死信路由键,会替换消息原来的路由键
        //args.put("x-dead-letter-routing-key", "dead");
        channel.queueDeclare(queueName, false, true, false, argos);
        //绑定,将队列和交换器通过路由键进行绑定
        channel.queueBind(queueName, DlxProducer.EXCHANGE_NAME, "#");
        System.out.println("waiting for message........");
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                //如果是cat的消息确认
                if (envelope.getRoutingKey().equals("cat")) {
                    System.out.println("Received[" + envelope.getRoutingKey() + "]" + message);
                    channel.basicAck(envelope.getDeliveryTag(), false);
                } else {
                    //如果是其他的消息拒绝(queue=false),成为死信消息
                    System.out.println("Will reject[" + envelope.getRoutingKey() + "]" + message);
                    channel.basicReject(envelope.getDeliveryTag(), false);
                }
            }
        };
        channel.basicConsume(queueName, false, consumer);
    }
}

3、申明一个消费者,负责消费死信队列

mport com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 类说明:普通的消费者,负责消费死信队列dlx_accept
 */
public class DlxConsumer {

    public final static String DLX_EXCHANGE_NAME = "dlx_accept";

    public static void main(String[] args) throws IOException, TimeoutException {
        //建立连接
        Connection connection = RabbitMQUtils.getConnection();
        // 创建一个信道
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(DLX_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String queueName = "dlx_accept";
        channel.queueDeclare(queueName, false, false, false, null);
        channel.queueBind(queueName, DLX_EXCHANGE_NAME, "#");
        System.out.println("waiting for message........");
        //声明了一个死信消费者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String message = new String(body, "UTF-8");
                System.out.println("Received dead letter[" + envelope.getRoutingKey() + "]" + message);
            }
        };
        //消费者正式开始在指定队列上消费消息
        channel.basicConsume(queueName, true, consumer);
    }
}

测试结果:

image-20211019141510180

DLX和备用交换器的区别

  1. 备用交换器是主交换器无法路由消息,那么消息将被路由到这个新的备用交换器,而死信交换器则是接收过期或者被拒绝的消息。
  2. 备用交换器是在声明主交换器时发生联系,而死信交换器则声明队列时发生联系。

场景分析:备用交换器一般是用于生产者生产消息时,确保消息可以尽量进入 RabbitMQ,而死信交换器主要是用于消费者消费消息产生死信的场景(比如消息过期,队列满了,消息拒绝且不重新投递)。

延时队列

延时队列,首先,它是一种队列,队列意味着内部的元素是有序的,元素出队和入队是有方向性的,元素从一端进入,从另一端取出。

其次,延时队列,最重要的特性就体现在它的延时属性上,跟普通的队列不一样的是,普通队列中的元素总是等着希望被早点取出处理,而延时队列中的元素则是希望被在指定时间得到取出和处理,所以延时队列中的元素是都是带时间属性的,通常来说是需要被处理的消息或者任务。

简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。

RabbitMQ是没有延时属性可以设置的,但是可以通过DLX+TTL的方式来实现 RabbitMQ 的延时队列,后面会有单独的文章来说明,或者可以看这篇文章:一文带你搞定RabbitMQ延迟队列

消息队列的控制

对于消费者而言,都是通过队列去获取数据,我们可以想想如果消息服务重启,那么之前的队列,交换机,消息是否还存在?对 RabbitMQ 而言是可控的,主要参数如下:

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;

临时队列

参数设置:durable = false

临时队列就是没有持久化的队列,也就是如果 RabbitMQ 服务器重启,那么这些队列就不会存在,所以我们称之为临时队列。

单消费者队列

参数设置:exclusive = true

普通队列允许的消费者没有限制,多个消费者绑定到多个队列时,RabbitMQ 会采用轮询进行投递。如果需要消费者独占队列,在队列创建的时候,设定属性 exclusive 为 true

自动删除队列

参数设置:autoDelete = false

自动删除队列和普通队列在使用上没有什么区别,唯一的区别是,当消费者断开连接时,队列将会被删除。自动删除队列允许的消费者没有限制,也就是说当这个队列上最后一个消费者断开连接才会执行删除。

自动删除队列只需要在声明队列时,设置属性 auto-delete 标识为 true 即可。系统声明的随机队列,缺省就是自动删除的。

自动过期队列

参数设置:arguments.put("x-expires",time)

指队列在超过一定时间没使用,会被从 RabbitMQ 中被删除,通过声明队列时,设定 x-expires 参数即可,单位毫秒。什么是没使用?

  1. 一定时间内没有 Get 操作发生。
  2. 没有 Consumer 连接在队列上。

永久队列

永久队列即持久化队列,持久化队列和非持久化队列的区别是,持久化队列会被保存在磁盘中,固定并持久的存储,当 Rabbit 服务重启后,该队列会保持原来的状态在 RabbitMQ 中被管理,而非持久化队列不会被保存在磁盘中,Rabbit 服务重启后队列就会消失。

非持久化比持久化的优势就是,由于非持久化不需要保存在磁盘中,所以使用速度就比持久化队列快。即是非持久化的性能要高于持久化。而持久化的优点就是会一直存在,不会随服务的重启或服务器的宕机而消失。

队列常用参数汇总

参数说明
x-dead-letter-exchange设置死信交换器
x-dead-letter-routing-key设置死信消息的可选路由键
x-expires队列在指定毫秒数后被删除
x-ha-policy创建HA(高可用)队列(后续文章)
x-ha-nodesHA队列的分布节点
x-max-length队列的最大消息数
x-message-ttl毫秒为单位的消息过期时间,队列级别
x-max-priority最大优先值为255的队列优先排序功能

Spring集成RabbitMQ

由于 Spring 基本上已经渗透到每个项目中去了,所以基于原生API不仅不好整合,而且写起来也很麻烦(当然,spring也是对原生API进行的包装),Spring 提供了一套自己的AMQP协议,主要就是用于 RabbitMQ 通过 AMQP 协议进行通信。话不多说,直接上手。

项目结构

本用例关于 RabbitMQ 的整合提供简单消息发送对象消费发送两种情况下的示例代码。

  1. BaseMessageListener 中声明了 topic 类型的交换机、持久化队列及其绑定关系,用于说明 topic 交换机的路由规则。

  2. ObjectMessageListener中声明了 direct 类型的交换机,持久化队列及其绑定关系,用于示例对象消息的传输。

image-20211020120137282

pom

新建一个普通的 spring 项目,引入以下依赖,注意 RabbitMQ 和 Spring 的版本要对应:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.javatv</groupId>
    <artifactId>spring-rabbitmq</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <spring-base-version>5.1.3.RELEASE</spring-base-version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring-base-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring-base-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring-base-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring-base-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring-base-version}</version>
        </dependency>
        <!--spring rabbitmq 整合依赖-->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
        <!--rabbitmq 传输对象序列化依赖了这个包-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>
        <!--单元测试相关包-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring-base-version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>spring-rabbitmq</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            <resource>
                <directory>src/main/java</directory>
            </resource>
        </resources>
    </build>

</project>

RabbitMQ配置

rabbitmq.properties

rabbitmq.addresses=192.168.153.128:5672
rabbitmq.username=admin
rabbitmq.password=admin
# 虚拟主机,可以类比为命名空间 默认为/,我这里是之前创建的
rabbitmq.virtualhost=order

rabbitmq.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation=
               "http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context.xsd
          http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/rabbit
          http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!-- rabbitmq连接配置文件 -->
    <context:property-placeholder location="rabbitmq.properties"/>

    <!-- 生产者  连接工厂 -->
    <rabbit:connection-factory id="connectionFactorys"
                               addresses="${rabbitmq.addresses}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtualhost}"/>
    <!--创建一个管理器(org.springframework.amqp.rabbit.core.RabbitAdmin),用于管理交换,队列和绑定。
    auto-startup 指定是否自动声明上下文中的队列,交换和绑定, 默认值为true。-->
    <rabbit:admin connection-factory="connectionFactorys" auto-startup="true"/>

    <!--声明 template 的时候需要声明id 如果有多个可能会抛出异常-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactorys"/>


    <!-- 可以在xml采用如下方式声明交换机、队列、绑定管理 -->
    <!-- 测试基础数据演示队列和交换机 begin -->
    <!-- 申明一个名为 spring.queue 队列 -->
    <rabbit:queue id="springQueue" name="spring.queue"/>

    <!-- 申明一个名为 spring_exchange_topic 交换机并和队列绑定 -->
    <rabbit:topic-exchange name="spring_exchange_topic">
        <!-- 可以绑定多个队列 -->
        <rabbit:bindings>
            <!-- 设置路由键  -->
            <rabbit:binding queue="springQueue" pattern="#"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    <!-- 测试基础数据演示队列和交换机 end -->

    <!-- 测试对象数据演示队列和交换机 begin -->
    <!-- 申明一个名为 spring.queue.object 队列 -->
    <rabbit:queue id="springQueueObject" name="spring.queue.object"/>

    <!-- 申明一个名为 spring_exchange_object_direct 交换机并和队列绑定 -->
    <rabbit:direct-exchange name="spring_exchange_object_direct">
        <!-- 可以绑定多个队列 -->
        <rabbit:bindings>
            <!-- 设置路由键  -->
            <rabbit:binding queue="springQueueObject" key="object"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>
    <!-- 测试对象数据演示队列和交换机 end -->


    <!-- 消费者 连接工厂 -->
    <!-- 创建一个监听器  类似于 @Bean  方法名为 rabbitListenerContainerFactory-->
    <bean id="rabbitListenerContainerFactory" class="org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory">
        <!-- 连接工厂 -->
        <property name="connectionFactory" ref="connectionFactorys" />
        <!-- 指定要创建的并发使用者数 -->
        <property name="concurrentConsumers" value="3" />
        <!-- 设置消费者数量的上限 -->
        <property name="maxConcurrentConsumers" value="10" />
    </bean>

    <!-- 配置consumer, 监听的类和queue的对应关系 -->
    <!-- none:不确认  auto:自动确认  manual:手动确认 -->
    <rabbit:listener-container connection-factory="connectionFactorys" acknowledge="manual" >
        <!-- 需要监听的队列,可以有多个,逗号隔开。如 springQueue,springQueue1 -->
        <rabbit:listener queues="springQueue" ref="baseMessageListener" />
        <rabbit:listener queues="springQueueObject" ref="objectMessageListener" />
    </rabbit:listener-container>

    <!--扫描rabbit包 自动声明交换器、队列、绑定关系-->
    <context:component-scan base-package="com.javatv.rabbit.listener"/>
</beans>

简单消息发送

这里的简单消息是指非实例对象的消息,如 String 类型的消息,队列等信息在上面的xml中已经配置完成。

消费者监听

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

@Component
public class BaseMessageListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            System.out.println("consumer:" + new String(message.getBody()));
            //手动确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //TODO 业务处理 mandatory
            e.printStackTrace();
            //消息拒绝,requeue = false  消息丢失,一般可采用DLX方式来处理
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

测试

import com.javatv.bean.Order;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.Date;

/**
 * @author ayue
 * @description 传输简单字符串
 */

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:rabbitmq.xml")
public class RabbitTest {

    public final static String EXCHANGE_NAME = "spring_exchange_topic";

    public final static String EXCHANGE_NAME_OBJECT = "spring_exchange_object_direct";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 一般消息测试
     */
    @Test
    public void sendMessage() {
        //设置消息
        MessageProperties properties = new MessageProperties();
        String received = "路由键为 ---> cat.blue.A 符合队列则会输出";
        Message message = new Message(received.getBytes(), properties);
        //发送消息
        rabbitTemplate.send(EXCHANGE_NAME, "cat.blue.A", message);
    }
}

运行测试可以在web端看见创建的队列和交换机:

image-20211020141156900

并且在控制台可以看到消费者监听并消费消息:

image-20211020141345489

对象消息发送

实际开发中一般都是实例对象,如下:

/**
 * 订单类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order implements Serializable {

    /**
     * 订单id
     */
    private String orderId;

    /**
     * 订单名称
     */
    private String name;

    /**
     * 下单时间
     */
    private Date orderTime;

    /**
     * 订单金额
     */
    private BigDecimal amount;
}

消费者监听

import com.javatv.bean.Order;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.stereotype.Component;

@Component
public class ObjectMessageListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        try {
            Order order = (Order) SerializationUtils.deserialize(message.getBody());
            System.out.println(order);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            //TODO 业务处理 mandatory
            e.printStackTrace();
            //消息拒绝,requeue = false  消息丢失,一般可采用DLX方式来处理
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }
}

测试

在测试类中添加如下测试代码:

@Test
public void sendOrder() {
    MessageProperties messageProperties = new MessageProperties();
    //传递的对象需要实现序列化接口
    Order order = new Order("1", "猫", new Date(), new BigDecimal("2000"));
    byte[] bytes = SerializationUtils.serialize(order);
    Message message = new Message(bytes, messageProperties);
    rabbitTemplate.send(EXCHANGE_NAME_OBJECT, "object", message);
}

同样可以在web端看到创建的队列和交换机信息:

image-20211020141953330

消费者控制台输出:

image-20211020142026567

高级配置RabbitMQ消息确认

消息确认包括主要发送确认和接收确认,因为发送消息的过程中我们是无法确认消息是否能路由等,一旦消息丢失我们就无法处理,所以需要确认消息,避免消息丢失。

我们把在原生API中的失败通知发送方确认模式集成到spring中。

1、把rabbitmq.xml中的模板转换器修改为如下,主要参数confirm-callback,return-callback,mandatory

<!-- 给模板指定转换器 声明 template 的时候需要声明id 如果有多个可能会抛出异常-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactorys"
                 confirm-callback="confirmCallBackListener"
                 return-callback="returnCallBackListener"
                 mandatory="true"/>

2、为了不和上述的队列冲突,这里新建一个队列来演示,添加到rabbitmq.xml即可:

<!-- 测试发送确认和失败确认 begin -->
<rabbit:queue id="advancedQueue" name="advanced.queue"/>
<!-- 申明一个名为 advanced_direct 交换机并和队列绑定 -->
<rabbit:direct-exchange name="advanced_direct">
    <rabbit:bindings>
        <!-- 设置路由键  -->
        <rabbit:binding queue="advancedQueue" key="advanced"/>
    </rabbit:bindings>
</rabbit:direct-exchange>
<!-- 测试发送确认和失败确认 end -->

3、为方便演示使用BaseMessageListener 普通消息监听,并把队列配置到监听器中:

image-20211020153041795

发送者失败通知ReturnCallback

生产者发送消息时设置 mandatory 标志,如果消息不可路由,将消息返回给发送者,并通知失败。

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback;
import org.springframework.stereotype.Component;

/**
 * 失败通知:失败后return回调
 */
@Component
public class ReturnCallBackListener implements ReturnCallback {
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        String msg = new String(message.getBody());
        System.out.println("返回的replyText :" + replyText);
        System.out.println("返回的exchange :" + exchange);
        System.out.println("返回的routingKey :" + routingKey);
        System.out.println("返回的message :" + msg);
    }
}

发送者确认回调ConfirmCallback

如果忘记是什么意思,可在上面章节中查看消息发布的权衡

import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ConfirmCallback;
import org.springframework.stereotype.Component;

/**
 * 发送方确认模式:确认后回调方
 */
@Component
public class ConfirmCallBackListener implements ConfirmCallback{
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("confirm--:correlationData:"+correlationData+",ack:"+ack+",cause:"+cause);
    }
}

测试

1、exchange,routingKey 都正确,发送者确认回调,ack=true,消息正常消费。

/**
 * exchange,routingKey 都正确,confirm被回调, ack=true
 *
 * @throws InterruptedException
 */
@Test
public void test1() throws InterruptedException {
    MessageProperties properties = new MessageProperties();
    String received = "exchange,routingKey 都正确,confirm被回调, ack=true";
    Message message = new Message(received.getBytes(), properties);
    rabbitTemplate.send(EXCHANGE_NAME, ROUTINGKEY, message);
    Thread.sleep(1000);
}

输出:

confirm--:correlationData:null,ack:true,cause:null
consumer:exchange,routingKey 都正确,confirm被回调, ack=true

2、exchange 错误,routingKey 正确,发送者确认回调,ack=false,找不到交换机。

/**
 * exchange 错误,routingKey 正确,confirm被回调, ack=false
 *
 * @throws InterruptedException
 */
@Test
public void test2() throws InterruptedException {
    MessageProperties properties = new MessageProperties();
    String received = "exchange 错误,queue 正确,confirm被回调, ack=false";
    Message message = new Message(received.getBytes(), properties);
    rabbitTemplate.send(EXCHANGE_NAME + "NO", ROUTINGKEY, message);
    Thread.sleep(1000);
}

输出:

confirm--:correlationData:null,ack:false,cause:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'advanced_directNO' in vhost 'order', class-id=60, method-id=40)

3、exchange 正确,routingKey 错误 ,发送者确认回调,ack=true,发送者失败通知。

/**
 * exchange 正确,routingKey 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
 *
 * @throws InterruptedException
 */
@Test
public void test3() throws InterruptedException {
    MessageProperties properties = new MessageProperties();
    String received = "exchange 正确,routingKey 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE";
    Message message = new Message(received.getBytes(), properties);
    rabbitTemplate.send(EXCHANGE_NAME, "", message);
    Thread.sleep(1000);
}

输出:

返回的replyText :NO_ROUTE
返回的exchange :advanced_direct
返回的routingKey :
返回的message :exchange 正确,routingKey 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
confirm--:correlationData:null,ack:true,cause:null

4、exchange 错误,routingKey 错误,发送者确认回调,ack=false

/**
 * exchange 错误,routingKey 错误,confirm被回调, ack=false
 *
 * @throws InterruptedException
 */
@Test
public void test4() throws InterruptedException {
    MessageProperties properties = new MessageProperties();
    String received = "exchange 错误,routingKey 错误,confirm被回调, ack=false";
    Message message = new Message(received.getBytes(), properties);
    rabbitTemplate.send(EXCHANGE_NAME + "NO", "", message);
    Thread.sleep(1000);
}

输出:

confirm--:correlationData:null,ack:false,cause:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'advanced_directNO' in vhost 'order', class-id=60, method-id=40)

完整测试用例:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:rabbitmq.xml")
public class RabbitAdvancedTest {

    public final static String EXCHANGE_NAME = "advanced_direct";

    public final static String ROUTINGKEY = "advanced";

    @Autowired
    private RabbitTemplate rabbitTemplate;


    /**
     * exchange,routingKey 都正确,confirm被回调, ack=true
     *
     * @throws InterruptedException
     */
    @Test
    public void test1() throws InterruptedException {
        MessageProperties properties = new MessageProperties();
        String received = "exchange,routingKey 都正确,confirm被回调, ack=true";
        Message message = new Message(received.getBytes(), properties);
        rabbitTemplate.send(EXCHANGE_NAME, ROUTINGKEY, message);
        Thread.sleep(1000);
    }

    /**
     * exchange 错误,routingKey 正确,confirm被回调, ack=false
     *
     * @throws InterruptedException
     */
    @Test
    public void test2() throws InterruptedException {
        MessageProperties properties = new MessageProperties();
        String received = "exchange 错误,queue 正确,confirm被回调, ack=false";
        Message message = new Message(received.getBytes(), properties);
        rabbitTemplate.send(EXCHANGE_NAME + "NO", ROUTINGKEY, message);
        Thread.sleep(1000);
    }

    /**
     * exchange 正确,routingKey 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
     *
     * @throws InterruptedException
     */
    @Test
    public void test3() throws InterruptedException {
        MessageProperties properties = new MessageProperties();
        String received = "exchange 正确,routingKey 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE";
        Message message = new Message(received.getBytes(), properties);
        rabbitTemplate.send(EXCHANGE_NAME, "", message);
        Thread.sleep(1000);
    }

    /**
     * exchange 错误,routingKey 错误,confirm被回调, ack=false
     *
     * @throws InterruptedException
     */
    @Test
    public void test4() throws InterruptedException {
        MessageProperties properties = new MessageProperties();
        String received = "exchange 错误,routingKey 错误,confirm被回调, ack=false";
        Message message = new Message(received.getBytes(), properties);
        rabbitTemplate.send(EXCHANGE_NAME + "NO", "", message);
        Thread.sleep(1000);
    }
}

SpringBoot集成RabbitMQ

由于 SpringBoot 是当前开发的一大趋势,看着上面复杂的 spring 配置,真的记不住啊,所以这里也提供一个SpringBoot 的方式。

项目结构

新建一个项目 spring-boot-rabbitmq,通过maven模块化方式构建三个子模块:

  • rabbitmq-common :公共模块,用于存放公共的接口、配置和 Java Bean,被 rabbitmq-producer 和 rabbitmq-consumer 在 pom.xml 中引用。
  • rabbitmq-producer :消息的生产者模块。
  • rabbitmq-consumer :是消息的消费者模块。

image-20211020171311215

这里只给出主要的mavne依赖,提供项目下载地址,可自行下载:

代码地址:https://gitee.com/javatv/advanced-way.git

pom

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

公共模块

image-20211020204356135

  • bean 下为公共的实体类。
  • constant 下为公共配置,用静态常量进行引用。这里我使用静态常量是为了方便引用,实际中也可以按照情况,抽取为公共的配置文件。

消息消费者

和集成spring一样,消费基本类型消息和对象消息。

image-20211020185121191

消费者配置

application.yml

spring:
  rabbitmq:
    addresses: 127.0.0.1:5672
    # RabbitMQ 默认的用户名和密码都是 guest 而虚拟主机名称是 "/"
    # 如果配置其他虚拟主机地址,需要预先用管控台或者图形界面创建 图形界面地址 http://主机地址:15672
    username: admin
    password: admin
    virtual-host: /
    listener:
      simple:
        # 为了保证信息能够被正确消费,建议签收模式设置为手工签收,并在代码中实现手工签收
        acknowledge-mode: manual
        # 侦听器调用者线程的最小数量
        concurrency: 10
        # 侦听器调用者线程的最大数量
        max-concurrency: 50

创建监听者

使用注解 @RabbitListener@RabbitHandler 创建消息的监听者,使用注解创建的交换机、队列、和绑定关系会在项目初始化的时候自动创建,但是不会重复创建。这里我们创建两个消息监听器,分别演示消息是基本类型和消息是对象时区别。

基本类型

import com.javatv.constant.RabbitInfo;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;

/**
 * @description : 消息消费者
 */
@Component
@Slf4j
public class RabbitmqConsumer {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = RabbitInfo.QUEUE_NAME, durable = RabbitInfo.QUEUE_DURABLE),
            exchange = @Exchange(value = RabbitInfo.EXCHANGE_NAME, type = RabbitInfo.EXCHANGE_TYPE),
            key = RabbitInfo.ROUTING_KEY)
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        MessageHeaders headers = message.getHeaders();
        // 获取消息头信息和消息体
        log.info("msgInfo:{} ; payload:{} ", headers.get("msgInfo"), message.getPayload());
        //  DELIVERY_TAG 代表 RabbitMQ 向该Channel投递的这条消息的唯一标识ID,是一个单调递增的正整数
        Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        // 第二个参数代表是否一次签收多条,当该参数为 true 时,则可以一次性确认 DELIVERY_TAG 小于等于传入值的所有消息
        channel.basicAck(deliveryTag, false);
    }
}

对象类型

import com.javatv.bean.Programmer;
import com.javatv.constant.RabbitBeanInfo;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @description : 消息是对象的消费者
 */

@Component
@Slf4j
public class RabbitmqBeanConsumer {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = RabbitBeanInfo.QUEUE_NAME, durable = RabbitBeanInfo.QUEUE_DURABLE),
            exchange = @Exchange(value = RabbitBeanInfo.EXCHANGE_NAME, type = RabbitBeanInfo.EXCHANGE_TYPE),
            key = RabbitBeanInfo.ROUTING_KEY)
    )
    @RabbitHandler
    public void onMessage(@Payload Programmer programmer, @Headers Map<String, Object> headers, Channel channel) throws Exception {
        log.info("programmer:{} ", programmer);
        Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        channel.basicAck(deliveryTag, false);
    }
}

消息生产者

image-20211020190147678

生产者配置

spring:
  rabbitmq:
    addresses: 127.0.0.1:5672
    # RabbitMQ 默认的用户名和密码都是 guest 而虚拟主机名称是 "/"
    # 如果配置其他虚拟主机地址,需要预先用管控台或者图形界面创建 图形界面地址 http://主机地址:15672
    username: admin
    password: admin
    virtual-host: /
    # 是否启用发布者确认 具体确认回调实现见代码
    publisher-confirms: true
    # 是否启用发布者返回 具体返回回调实现见代码
    publisher-returns: true
    # 是否启用强制消息 保证消息的有效监听
    template.mandatory: true

server:
  port: 8090

创建生产者

该生产者包括了失败通知和确认回调。

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.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @description : 消息生产者
 */
@Component
@Slf4j
public class RabbitmqProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendSimpleMessage(Map<String, Object> headers, Object message, String messageId, String exchangeName, String key) {
        // 自定义消息头
        MessageHeaders messageHeaders = new MessageHeaders(headers);
        // 创建消息
        Message<Object> msg = MessageBuilder.createMessage(message, messageHeaders);
        /* 确认的回调 确认消息是否到达 Broker 服务器 其实就是是否到达交换器
         * 如果发送时候指定的交换器不存在 ack 就是 false 代表消息不可达
         */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            log.info("correlationData:{} , ack:{}", correlationData.getId(), ack);
            if (!ack) {
                System.out.println("进行对应的消息补偿机制");
            }
        });
        /* 消息失败的回调
         * 例如消息已经到达交换器上,但路由键匹配任何绑定到该交换器的队列,会触发这个回调,此时 replyText: NO_ROUTE
         */
        rabbitTemplate.setReturnCallback((message1, replyCode, replyText, exchange, routingKey) -> {
            log.info("message:{}; replyCode: {}; replyText: {} ; exchange:{} ; routingKey:{}",
                    message1, replyCode, replyText, exchange, routingKey);
        });
        // 在实际中ID 应该是全局唯一 能够唯一标识消息 消息不可达的时候触发ConfirmCallback回调方法时可以获取该值,进行对应的错误处理
        CorrelationData correlationData = new CorrelationData(messageId);
        rabbitTemplate.convertAndSend(exchangeName, key, msg, correlationData);
    }
}

测试

为了简要的测试,这里直接通过单元测试的方法进行测试。

1、启动生产者RabbitmqProducerApplication和消费者RabbitmqConsumerApplication

2、测试类生产消息。

import com.javatv.bean.Order;
import com.javatv.constant.RabbitBeanInfo;
import com.javatv.constant.RabbitInfo;
import com.javatv.rabbitmq.producer.RabbitmqProducer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitmqProducerTests {

    @Autowired
    private RabbitmqProducer producer;

    /***
     * 发送消息体为简单数据类型的消息
     */
    @Test
    public void send() {
        Map<String, Object> heads = new HashMap<>();
        heads.put("msgInfo", "自定义消息头信息");
        // 模拟生成消息ID,在实际中应该是全局唯一的 消息不可达时候可以在setConfirmCallback回调中取得,可以进行对应的重发或错误处理
        String id = String.valueOf(Math.round(Math.random() * 10000));
        producer.sendMessage(heads, "hello Spring", id, RabbitInfo.EXCHANGE_NAME, RabbitInfo.ROUTING_KEY);
    }


    /***
     * 发送消息体为bean的消息
     */
    @Test
    public void sendBean() {
        String id = String.valueOf(Math.round(Math.random() * 10000));
        Order order = new Order("1", "猫", new Date(), new BigDecimal("2000"));
        producer.sendMessage(null, order, id, RabbitBeanInfo.EXCHANGE_NAME, RabbitBeanInfo.ROUTING_KEY);
    }
}

3、查看客户端输出日志。

image-20211020204030613

在实际工作中,一般是 xml 和SpringBoot注解方式结合起来用,这里仅仅是一个简单的测试用例,如果你项目中有在使用,可以看看配置有何不同,如果你还不了解,可以把它当做入门。

后续关于 RabbitMQ 的文章:

  • RabbitMQ 应用解耦(订单系统和库存系统分离)
  • RabbitMQ 补偿机制、消息幂等性、最终一致性、消息顺序性问题
  • RabbitMQ 延迟队列(DLX+TTL)的实际运用(订单支付超时取消问题)
  • RabbitMQ 集群和集群高可用(HAProxy

参考: Spring整合RabbitMQ (xml配置方式)

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-10-21 12:03:18  更:2021-10-21 12:04:49 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 22:39:51-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码