一、 RabbitMQ概述
1.1 什么是消息队列
消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。不建议传递对象,如果需要传递复杂数据建议传递Json。
消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
1.2 为什么要使用消息队列
上面的描述中可以看出消息队列是一种应用间的异步协作机制,那什么时候需要使用 MQ 呢?
以常见的订单系统为例,用户点击【下单】按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发红包、发短信通知。在业务发展初期这些逻辑可能放在一起同步执行,随着业务的发展订单量增长,需要提升系统服务的性能,这时可以将一些不需要立即生效的操作拆分出来异步执行,比如发放红包、发短信通知等。这种场景下就可以用 MQ ,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ 让主流程快速完结,而由另外的单独线程拉取MQ的消息(或者由 MQ 推送消息),当发现 MQ 中有发红包或发短信之类的消息时,执行相应的业务逻辑。
以上是用于业务解耦的情况,其它常见场景包括最终一致性、广播、错峰流控等等。
1.3 RabbitMQ特点
RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。具体特点包括:
1、可靠性(Reliability)
RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
2、灵活的路由(Flexible Routing)
在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
3、 消息集群(Clustering)
多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
高可用(Highly Available Queues)
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
4、多种协议(Multi-protocol)
RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
5、多语言客户端(Many Clients)
RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
6、 管理界面(Management UI)
RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
7、 跟踪机制(Tracing)
如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
1.4、不同MQ产品的区别
二、 RabbitMQ安装
一般来说安装 RabbitMQ 之前要安装 Erlang ,可以去Erlang官网下载。接着去RabbitMQ官网下载安装包,之后解压缩即可。
Erlang官方下载地址:Downloads - Erlang/OTP
RabbitMQ官方下载地址:Downloading and Installing RabbitMQ — RabbitMQ
2.1 安装前的准备
2.1.1 依赖包安装
安装RabbitMQ之前必须要先安装所需要的依赖包可以使用下面的一次性安装命令
yum install gcc glibc-devel make ncurses-devel openssl-devel xmlto -y
2.1.2 安装Erlang
1、 将Erlang源代码包otp_src_19.3.tar.gz上传到Linux的/home目录下
2、解压erlang 源码包
tar -zxvf otp_src_19.3.tar.gz
3、手动创建erlang 的安装目录
mkdir /usr/local/erlang
4、进入erlang的解压目录
cd otp_src_19.3
5、配置erlang的安装信息
./configure --prefix=/usr/local/erlang --without-javac
6、编译并安装
make && make install
7、配置环境变量
vim /etc/profile
8、将这些配置填写到profile文件的最后
ERL_HOME=/usr/local/erlang
PATH=PATH
export ERL_HOME PATH
9、启动环境变量配置文件
source /etc/profile
2.2 安装RabbitMQ
1、 将RabbitMQ安装包rabbitmq-server-3.7.2-1.el7.noarch.rpm上传到/home目录
2、安装RabbitMQ
rpm -ivh --nodeps rabbitmq-server-3.7.2-1.el7.noarch.rpm
2.3 RabbitMQ常用命令
2.3.1 启动和关闭
1、启动RabbitMQ
rabbitmq-server start &
注意:这里可能会出现错误,错误原因是/var/lib/rabbitmq/.erlang.cookie文件权限不够。
解决方案对这个文件授权
chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
chmod 400 /var/lib/rabbitmq/.erlang.cookie
2、停止服务
rabbitmqctl stop
2.3.2 插件管理
1、添加插件
rabbitmq-plugins enable {插件名}
2、删除插件
rabbitmq-plugins disable {插件名}
注意:RabbitMQ启动以后可以使用浏览器进入管控台但是默认情况RabbitMQ不允许直接使用浏览器浏览器进行访问因此必须添加插件
rabbitmq-plugins enable rabbitmq_management
3、使用浏览器访问管控台http://RabbitMQ服务器IP:15672
2.3.3 用户管理
RabbitMQ安装成功后使用默认用户名guest登录
账号:guest
密码:guest
注意:这里guest只允许本机登录访问需要创建用户并授权远程访问命令如下
1、 添加用户:rabbitmqctl add_user {username} {password}
rabbitmqctl add_user root root
2、 删除用户:rabbitmqctl delete_user {username}
3、 修改密码:rabbitmqctl change_password {username} {newpassword}
rabbitmqctl change_password root 123456
4、 设置用户角色:rabbitmqctl set_user_tags {username} {tag}
rabbitmqctl set_user_tags root administrator
tag参数表示用户角色取值为:management ,monitoring ,policymaker administrator
各角色详解:
management
用户可以通过AMQP做的任何事外加:
列出自己可以通过AMQP登入的virtual hosts
查看自己的virtual hosts中的queues, exchanges 和 bindings
查看和关闭自己的channels 和 connections
查看有关自己的virtual hosts的“全局”的统计信息,包含其他用户在这些virtual hosts中的活动。
policymaker
management可以做的任何事外加:
查看、创建和删除自己的virtual hosts所属的policies和parameters
monitoring
management可以做的任何事外加:
列出所有virtual hosts,包括他们不能登录的virtual hosts
查看其他用户的connections和channels
查看节点级别的数据如clustering和memory使用情况
查看真正的关于所有virtual hosts的全局的统计信息
administrator
policymaker和monitoring可以做的任何事外加:
创建和删除virtual hosts
查看、创建和删除users
查看创建和删除permissions
关闭其他用户的connections
2.3.4 权限管理
1、 授权命令:rabbitmqctl set_permissions [-p vhostpath] {user} {conf} {write} {read}
-p vhostpath :用于指定一个资源的命名空间,例如 –p / 表示根路径命名空间
user:用于指定要为哪个用户授权填写用户名
conf:一个正则表达式match哪些配置资源能够被该用户配置。
write:一个正则表达式match哪些配置资源能够被该用户读。 read:一个正则表达式match哪些配置资源能够被该用户访问。
例如:
rabbitmqctl set_permissions -p / root '.*' '.*' '.*'
用于设置root用户拥有对所有资源的 读写配置权限
2、查看用户权限 rabbitmqctl list_permissions [vhostpath]
例如
查看根径经下的所有用户权限
rabbitmqctl list_permissions
查看指定命名空间下的所有用户权限
rabbitmqctl list_permissions /abc
3、查看指定用户下的权限rabbitmqctl list_user_permissions {username}
例如
查看root用户下的权限
rabbitmqctl list_user_permissions root
4、清除用户权限rabbitmqctl clear_permissions {username}
例如:
清除root用户的权限
rabbitmqctl clear_permissions root
2.3.5 vhost管理
vhost是RabbitMQ中的一个命名空间,可以限制消息的存放位置利用这个命名空间可以进行权限的控制有点类似Windows中的文件夹一样,在不同的文件夹中存放不同的文件。
1、添加vhost: rabbitmqctl add vhost {name}
例如
rabbitmqctl add vhost abc
2、删除vhost:rabbitmqctl delete vhost {name}
例如
rabbitmqctl delete vhost abc
三、 RabbitMQ消息发送和接收
所有 MQ 产品从模型抽象上来说都是一样的过程: 消费者(consumer)订阅某个队列。生产者(producer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者。
1、Message
消息,消息是不具体的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher 消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange 交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding 绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue 消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
6、Connection 网络连接,比如一个TCP连接。
7、Channel 信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8、Consumer 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host 虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker 表示消息队列服务器实体。
3.2 AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列
3.3 Exchange 类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型
1、direct
消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
2、fanout
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
3、topic
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配不多不少一个单词。
3.4 Java发送和接收Queue的消息
rabbitmq 五种消息分发机制:
1.单收单发:
2.工作队列
基于交换机(exchange)的有三种:
1.发布/订阅
2.路由匹配:全匹配
3.topics:通配符
四、简单模式(单收单发)
4.1 创建Maven工程,创建consumer、produce模块
2个模块添加maven依赖
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.1.1</version>
</dependency>
</dependencies>
4.2 produce编写消息发送类
创建一个获取connection连接的工具类
模块编写消息发送类
package com.rabbitmq.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ProducerSimpleTest {
//3.设置队列名称
static final String QUEUE_NAME = "simpleQueue";
public static void main(String[] args) throws IOException, TimeoutException {
//1、获取连接
Connection connection = ConnectionUtil.getConnection();
//2、在连接中创建连接通道
Channel channel = connection.createChannel();
//3、创建队列,并描述队列信息
/*
* queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5)
* 参数1:队列名称
* 参数2:定义队列是否持久化(一般都是true,我们不可以调一次,创建一次队列后,用完在销毁,太耗费性能)
* 参数3:消费者是否独占这次连接(意思是一个消息只能被一个消费者消费,一般都是false,单线程消费,浪费性能)
* 参数4:是否在不在使用服务的时候删除队列
* 参数5:队列的其他参数设定
*
*/
channel.queueDeclare(QUEUE_NAME, true,false, false, null);
String message = "我是一条consumer发送的消息";
//4、发送消息到mq服务
/*
* void basicPublish(String var1, String var2, BasicProperties var3, byte[] var4)
* 参数1:交换机名称,简单模式下,默认使用8个交换机中的默认交换机,缺省名称,不需要写
* 参数2:交换机binding到队列的方式,传递的名字就是队列名称,就会通过名称找到对应的队列,消息发送
* 参数3:发送消息的时候,携带的其他属性
* 参数4:消息内容
* */
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
//如果关闭了,以后服务就不能再使用了,先注释
/*channel.close();
connection.close();*/
}
}
运行,发送消息,查看mq队列信息
我们看到已经有了一条消息
4.3 consumer编写消息消费类
package com.rabbitmq.simple;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerSimpleTest {
static final String QUEUE_NAME = "simpleQueue";
public static void main(String[] args) throws IOException, TimeoutException {
//1、从工具类中获取连接对象
Connection connection = ConnectionUtil.getConnection();
//2、创建通道
Channel channel = connection.createChannel();
//3、调用消费的方法对于MQ队列中的消息进行消费
/**
* 参数1:队列名称
* 参数2:是否确定消费状态,如果为true,表示收到了消息,发送一个确认消息给mq,mq拿到了这个确认消息以后
* 会将队列中的消息进行删除,如果设置为false,需要手动确认。
* 参数3:回调函数 Consumer对象
*
*/
/**
* 因为需要传递 Consumer对象,Consumer是一个接口,接口不能new,因为我们使用Consumer接口的实现类DefaultConsumer
* DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
* public void handleDelivery{}
* }
* 这个是创建DefaultConsumer类对象,并且重写DefaultConsumer类中的handleDelivery方法。如果我们创建DefaultConsumer类对象
* 调用handleDelivery方法,调用就是我们重写的方法
*/
Consumer defaultConsumer = new DefaultConsumer(channel){
/**
* 消息规则
* 该方法定义了如何进行消费的策略
* @param consumerTag 消费消息的标签
* @param envelope 消费的消息打包在这个对象中 消息id、消息rountingkey 交换机 ...
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消息的标签是:" + consumerTag);
System.out.println("消费者的路由/消息routingkey:" + envelope.getRoutingKey());
System.out.println("消息id:" + envelope.getDeliveryTag());
System.out.println("交换机:" + envelope.getExchange());
System.out.println("属性:" + properties);
System.out.println("消费的消息是:" + new String(body));
}
};
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
运行消费类,查看控制台信息
查看mq客户端,mq消息是否被消费
显示已经没有消息了
注意:
1、Queue的消息只能被同一个消费者消费,如果没有消费监听队列那么消息会存放到队列中持久化保存,直到有消费者来消费这个消息,如果以有消费者监听队列则立即消费发送到队列中的消息
2、Queue的消息可以保证每个消息都一定能被消费
五、工作模式
消息消费产生消息,将消息发送到消息队列中,这是竞争,消费者1和消费者2都监听消息队列,当队列中有消息,一起来抢消息。谁抢到谁处理。
1、 编写生产者类
package com.rabbitmq.work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ProducerWorkTest {
static final String QUEUE_NAME = "workQueue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection connection = ConnectionUtil.getConnection();
//创建连接通道
Channel channel = connection.createChannel();
/**
* 创建队列
* 参数1:队列的名字
* 参数2:定义队列是否持久化
* 参数3:消费者是否独占本次连接
* 参数4:是否在不在使用服务的时候删除队列
* 参数5:队列的其他参数设定
* queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5)
*/
channel.queueDeclare(QUEUE_NAME, true, false, true, null);
//循环发送10次消息
for (int i = 0; i < 10; i++) {
/**
* void basicPublish(String var1, String var2, BasicProperties var3, byte[] var4)
* 参数1:交换机的名字,简单模式下,默认使用8个交换机中的默认交换机,缺省名称,不需要写
* 参数2:交换机binding到队列的方式,传递的名字就是队列名称,就会通过名称找到对应的队列,消息发送
* 参数3:发送消息的时候,携带的其他属性
* 参数4:消息内容
*/
String message = "我是第---"+ i+"---次mq消息";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
}
}
}
运行,查看mq,已生成10条消息
2、编写消费者1
package com.rabbitmq.work;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerWorkTest1 {
static final String QUEUE_NAME = "workQueue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection connection = ConnectionUtil.getConnection();
//创建连接通道
Channel channel = connection.createChannel();
//创建Consumer对象,对象中主要定义了消费的规则
Consumer consumer = new DefaultConsumer(channel) {
/*
该方法定义了如何进行消费的策略
参数1:String consumerTag 消费的标签
参数2:Envelope envelope 消费的消息打包封装在这个对象中 消息id 消息rountingkey 交换机 ...
参数3:properties 属性信息
参数4:body消息
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("ConsumerWorkTest1接收到mq消息是" + new String(body));
}
};
//调用消费的方法对于MQ队列中的消息进行消费
/*
参数1:队列名称
参数2:是否确定消费状态,如果为true,表示收到了消息,发送一个确认消息给mq,mq拿到了这个确认消息以后
会将队列中的消息进行删除,如果设置为false,需要手动确认
参数3: callback回调,当拿到这条消息以后调用的操作
消费策略
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
3、编写消费者2
package com.rabbitmq.work;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerWorkTest2 {
static final String QUEUE_NAME = "workQueue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("ConsumerWorkTest2接收到mq消息是:" + new String(body));
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
注意:我们先运行 生产者类,然后我们运行消费者1,我们发现消费者1将所有消息都已经消费了,我们在运行消费者2后,我们再一次运行生产者,此时就可以看出消费者1消费者2同时消费的情况了
我们按照上面方式运行
从上面的消费情况我们可以看出,2个消费者竞争,消费者1消费一条,消费者2消费一条,是有序的,不是随机消费。
六、基于交换机(exchange)的三种模式
AMQP 协议中的核心思想就是生产者和消费者的解耦,生产者从不直接将消息发送给队列。生产者通常不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。先由 Exchange 来接收,然后 Exchange 按照特定的策略转发到 Queue 进行存储。Exchange 就类似于一个交换机,将各个消息分发到相应的队列中。
在实际应用中我们只需要定义好 Exchange 的路由策略,而生产者则不需要关心消息会发送到哪个 Queue 或被哪些 Consumer 消费。在这种模式下生产者只面向 Exchange 发布消息,消费者只面向 Queue 消费消息,Exchange 定义了消息路由到 Queue 的规则,将各个层面的消息传递隔离开,使每一层只需要关心自己面向的下一层,降低了整体的耦合度。
6.1、发布/订阅
消息产生者产生消息,将消息发送到交换机中。多个消息队列绑定到交换机上。交换机将消息发送到多个队列中。消费者1监听自己的队列,如果有消息就进行消费。消费者2监听自己的队列,如果有消息进行消费。(通过交换机将消息同时发送到多个队列中,不同队列绑定的消费者,消费对应队列的消息)
编写生产者
package com.rabbitmq.publishSubscribe;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class PublishSubscribeProducerTest {
//定义交换机名称
static final String FANOUT_EXCHANGE = "fanout_exchange";
//定义2个队列名称
static final String FANOUT_QUEUE_1 = "fanout_queue_1";
static final String FANOUT_QUEUE_2 = "fanout_queue_2";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建连接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.创建交换机
/**
*DeclareOk exchangeDeclare(String var1, BuiltinExchangeType var2)
* 参数1:交换机名称
* 参数2:交换机类型
* fanout:广播类型,发送消息到每个交换机绑定的队列中
* direct:定向类型,自定义routing key,然后将消息按照定义的的routing key进行匹配队列并发送
* topic:通配符类型,允许进行模糊匹配,符合匹配规则的都会被发送到对应的队列中
*/
channel.exchangeDeclare(FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
//4.创建队列
/*
queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5)
参数1:队列的名字
参数2:定义队列是否持久化
参数3:消费者是否独占本次连接
参数4:是否在不在使用服务的时候删除队列
参数5:队列的其他参数设定
*/
channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null);
//5、交换机绑定队列
/**
* 参数1:队列名称
* 参数2:交换机名称
* 参数3:如果交换机的模式不是BuiltinExchangeType.FANOUT(广播模式),则需要指定分发消息时指定的匹配规则
*但如果是(广播模式),将消息发送到所有绑定的队列,不需要指定匹配规则
*/
channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHANGE, "");
channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHANGE, "");
//6、发送消息
/*
生产者将消息发送到mq,mq会将消息给到交换机,由于交换机是FANOUT,将消息给到所有和他绑定的队列
void basicPublish(String var1, String var2, BasicProperties var3, byte[] var4)
参数1:交换机的名字,简单模式下,默认使用8个交换机中的默认交换机,缺省名称,不需要写
参数2:交换机binding到队列的方式,传递的名字就是队列名称,就会通过名称找到对应的队列,消息发送
参数3:发送消息的时候,携带的其他属性
参数4:消息内容
*/
String message = "这是通过交换以广播默认发送给所有队列的消息";
channel.basicPublish(FANOUT_EXCHANGE, "", null, message.getBytes(StandardCharsets.UTF_8));
}
}
消费者1
package com.rabbitmq.publishSubscribe;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublishSubscribeConsumerTest1 {
static final String FANOUT_QUEUE_1 = "fanout_queue_1";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建连接
Connection connection = ConnectionUtil.getConnection();
//2.创建连接通道
Channel channel = connection.createChannel();
//3、消费消息
/*
参数1:队列名称
参数2:是否确定消费状态,如果为true,表示收到了消息,发送一个确认消息给mq,mq拿到了这个确认消息以后
会将队列中的消息进行删除,如果设置为false,需要手动确认
参数3: callback回调,当拿到这条消息以后调用的操作
*/
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("PublishSubscribeConsumerTest1消费的消息是:" + new String(body));
}
};
//完成消费
channel.basicConsume(FANOUT_QUEUE_1, true, consumer);
}
}
消费者2
package com.rabbitmq.publishSubscribe;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublishSubscribeConsumerTest2 {
static final String FANOUT_QUEUE_2 = "fanout_queue_2";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("PublishSubscribeConsumerTest2消费的消息是:" + new String(body));
}
};
channel.basicConsume(FANOUT_QUEUE_2, true, consumer);
}
}
先启动消费者1、2,在启动生产者
从上面看出,两个消费者都消费各个队列的消息了,并且消息是一致的。
6.2、路由模式
比发布订阅模式多了一个路由选择,称为路由key。路由key指定一个名称。队列在绑定到交换机时,还要设置这个路由key。消息的队列中不是所有的消息了,交换机会根据消息的路由key,选择性将消息传递给消息队列
6.2.1、编写direct消息发送类
package com.rabbitmq.routing;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ProducerRoutingTest {
//交换机名称
static final String DIRECT_EXCHANGE = "direct_exchange";
//定义2个队列名称
static final String DIRECT_QUEUE_1 = "direct_queue_1";
static final String DIRECT_QUEUE_2 = "direct_queue_2";
public static void main(String[] args) throws IOException, TimeoutException {
//1.获取连接
Connection connection = ConnectionUtil.getConnection();
//2.创建通道
Channel channel = connection.createChannel();
//3.创建交换机
/**
* 参数1:交换机名称
* 参数2:交换机类型
* 交换机的类型:
* fanout:广播类型,发送消息到每个交换机绑定的队列中
* direct:定向类型,自定义routing key,然后将消息按照定义的的routing key进行匹配队列并发送
* topic:通配符类型,允许进行模糊匹配,符合匹配规则的都会被发送到对应的队列中
*
*/
channel.exchangeDeclare(DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);
//4.创建队列
/*
queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5)
参数1:队列的名字
参数2:定义队列是否持久化
参数3:消费者是否独占本次连接
参数4:是否在不在使用服务的时候删除队列
参数5:队列的其他参数设定
*/
channel.queueDeclare(DIRECT_QUEUE_1, true, false, false, null);
channel.queueDeclare(DIRECT_QUEUE_2, true, false, false, null);
//5.绑定交换机与队列,描述交换机与队列绑定时的routingkey的规则,拿到消息才知道发送到那个队列
/**
*
* 参数1:队列名称
* 参数2:交换机名称
* 参数3:如果交换机的模式不是BuiltinExchangeType.FANOUT(广播模式),则需要指定分发消息时指定的匹配规则(routingkey)
* 但如果是(广播模式),将消息发送到所有绑定的队列,不需要指定匹配规则
*/
channel.queueBind(DIRECT_QUEUE_1, DIRECT_EXCHANGE, "error");
channel.queueBind(DIRECT_QUEUE_2, DIRECT_EXCHANGE, "info");
channel.queueBind(DIRECT_QUEUE_2, DIRECT_EXCHANGE, "warning");
channel.queueBind(DIRECT_QUEUE_2, DIRECT_EXCHANGE, "error");
//6.发送消息,将消息根据routingkey发送对应的队列中
/*
生产者将消息发送到mq,mq会将消息给到交换机,由于交换机是FANOUT,将消息给到所有和他绑定的队列
void basicPublish(String var1, String var2, BasicProperties var3, byte[] var4)
参数1:交换机的名字,简单模式下,默认使用8个交换机中的默认交换机,缺省名称,不需要写
参数2:交换机binding到队列的方式,传递的名字就是队列名称,就会通过名称找到对应的队列,消息发送
参数3:发送消息的时候,携带的其他属性
参数4:消息内容
*/
String info_message = "这是一条info级别的消息";
channel.basicPublish(DIRECT_EXCHANGE, "info",null, info_message.getBytes(StandardCharsets.UTF_8));
String error_message = "这是一条error级别的消息";
//会给routingkey为error的队列中发送,队列1、队列2中都会收到
channel.basicPublish(DIRECT_EXCHANGE, "error", null, error_message.getBytes(StandardCharsets.UTF_8));
}
}
我们运行一下提供者,查看一下mq客户端
从上面看,队列2中存在两条信息,队列1中存在一条,我们上面的代码routingkey 是info只有队列2中存在,routingkey是error队列1、队列2都存在,因此队列2中存在2条消息,一条info,一条error。队列1中只有一条error消息
6.2.2、编写direct消费者类
ConsumerRoutingTest1类
package com.rabbitmq.routing;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerRoutingTest1 {
//定义2个队列名称
static final String DIRECT_QUEUE_1 = "direct_queue_1";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Consumer consumer = new DefaultConsumer(channel){
/**
* 消息规则
* 该方法定义了如何进行消费的策略
* @param consumerTag 消费消息的标签
* @param envelope 消费的消息打包在这个对象中 消息id、消息rountingkey 交换机 ...
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("ConsumerRoutingTest1收到的消息是:" + new String(body));
System.out.println("消息写入数据库");
}
};
channel.basicConsume(DIRECT_QUEUE_1, true, consumer);
}
}
ConsumerRoutingTest2类
package com.rabbitmq.routing;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerRoutingTest2 {
static final String DIRECT_QUEUE_2 = "direct_queue_2";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("ConsumerRoutingTest2接收到的消息"+new String(body));
System.out.println("消息写入日志文件");
}
};
channel.basicConsume(DIRECT_QUEUE_2,true, consumer);
}
}
我们先运行生产者,在运行消费者
我们可以看出Test1消费者,消费队列1中的error消息。Test2消费者,消费队列2中的info以及error消息。我们可以看出如果队列中有多条消息会一次性都给消费。
6.3、主题模式
在路由模式基础上,让路由key可以使用通配符。相当于进行分类。灵活程度更高些。隐患:容易误伤。
注意: *表示一个单词,#表示2个单词
6.3.1、编写topic类型,消息发送类
package com.rabbitmq.topics;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class ProducerTopicTest {
final static String TOPIC_EXCHANGE = "topic_exchange";
static final String TOPIC_QUEUE_1 = "topic_queue_1";
static final String TOPIC_QUEUE_2 = "topic_queue_2";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);
channel.queueDeclare(TOPIC_QUEUE_1,true,false,false,null);
channel.queueDeclare(TOPIC_QUEUE_2,true,false,false,null);
/**
* 交换机与队列绑定
* 这次的rountingkey不是固定的是,而是一种统配方式的tountingkey
* * :表示一个单词
* # :表示多个单词 例如
*/
channel.queueBind(TOPIC_QUEUE_1, TOPIC_EXCHANGE, "product.*");
channel.queueBind(TOPIC_QUEUE_2, TOPIC_EXCHANGE, "#.error");
String message = "商品交易信息-info级别";
/**
* 发送消息
* product.info 表示rountingkey,这个rountingkey符合交换机绑定队列的规则的统配方式,因此会将消息发送到TOPIC_QUEUE_1队列中
*/
channel.basicPublish(TOPIC_EXCHANGE, "product.info", null, message.getBytes(StandardCharsets.UTF_8));
String message1 = "商品交易信息-error级别";
channel.basicPublish(TOPIC_EXCHANGE, "product.error", null, message1.getBytes(StandardCharsets.UTF_8));
}
}
运行,我们查看mq控制台,我们看出队列1中有两条消息,队列2中只有1条。
因为上面的message,发送的rountingkey是product.info,这个只符合队列1的rountingkey匹配规则,而mssage1,发送的rountingkey是product.error,这个既符合队列1rountingkey匹配规则,也符合队列2rountingkey匹配规则,因此队列1中2条,队列2中一条
6.3.2、编写topic类型,消息消费者类1
package com.rabbitmq.topics;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicConsumerTest1 {
static final String TOPIC_QUEUE_1 = "topic_queue_1";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("TopicConsumerTest1消费商品相关的信息,写入到日志中:" + new String(body));
}
};
channel.basicConsume(TOPIC_QUEUE_1, true, consumer);
}
}
6.3.3、编写topic类型,消息消费者类2
package com.rabbitmq.topics;
import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TopicConsumerTest2 {
static final String TOPIC_QUEUE_2 = "topic_queue_2";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("TopicConsumerTest2消费错误的消息,将其写入到数据库" + new String(body));
}
};
channel.basicConsume(TOPIC_QUEUE_2, true, consumer);
}
}
运行消费者1、消费者2
我们看出队列1中有两条消息,都已被消费者1消费,队列2中有一条消息,也已经被消费者2消费。
七、mq使用可能会出现的问题
7.1、mq消费顺序,可能与预计的不一致
解决办法:将必须要保证顺序的消息放在一个队列中,队列的消费机制就是先进先出,后进后出
7.2、 重复消费问题
一条消息,消费多次
解决办法:消费完成,引入一个记录消费顺序的节点的组件,比如redis或者Map等;
7.3、消息丢失问题
八、springboot集成rabbitmq
8.1、编写生产者模块
8.1.1、创建生产者模块
创建生产者模块,选择rabbitmq插件
8.1.2、核心配置文件配置mq信息
8.1.3、创建config包下创建RabbitMQConfig配置类
package com.springboot.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
//指定交换机名称
public static final String EXCHANGE_NAME = "boot_topic_exchange";
//指定队列名称
public static final String QUEUE_NAME = "boot_queue";
/**
* 创建交换机对象
* @return
*/
@Bean("bootExchange")
public Exchange bootExchange(){
/*
创建交换机指定名称,并指定交换机是持久化的
*/
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
/**
* 创建队列
* @return
*/
@Bean("bootQueue")
public Queue bootQueue(){
/*
创建队列指定名称,并指定队列是持久化的
durable 持久化
noDurable 不持久化
*/
return QueueBuilder.durable(QUEUE_NAME).build();
}
/**
* 绑定将队列绑定到交换机上,并且设置rountingkey匹配规则
* @param exchange 交换机bean对象
* @param queue 队列bean对象
* @return
*/
@Bean
public Binding bindingQueueExchange(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue){
//绑定交换机和队列,并指定了routingkey
return BindingBuilder.bind(queue).to(exchange).with("springboot.#").noargs();
}
}
8.1.4、测试包下,创建测试类,测试配置类代码是否正确
package com.springboot.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProducerTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testSend(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"springboot.java","springboot基础rabbitmq生产者生产消息测试");
}
}
运行,查看mq客户端,队列中生成一条消息
8.2、编写消费者模块
8.2.1、创建消费者模块,选择插件,并且配置核心配置文件(同上)
8.2.2、编写消费者监听
消费者模块,创建监听包
创建RabbitMQListener类
package com.springboot.listener;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* rabbitmq监听类
*/
@Component
public class RabbitMQListener {
//指定要消费的队列名称
public static final String QUEUE_NAME = "boot_queue";
/**
* 创建一个监听 @RabbitListener(queues = QUEUE_NAME)
* queues监听的队列名称
* springboot启动后,会监听该队列是否有消息,有消息就进行消费
* @param message
*/
@RabbitListener(queues = QUEUE_NAME)
public void listenerQueue(Message message){
System.out.println(new String(message.getBody()));
}
}
spring启动后,对boot_queue队列进行监听,队列中有消息,则进行消费。
启动spring
查看消息已经进行消费了