1. MQ 简介
MQ
(Message Quene) : 翻译为 消息队列
,通过典型的 生产者
和消费者
模型,生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,轻松的实现系统间解耦。别名为 消息中间件
,通过利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
当今市面上有很多主流的消息中间件,如老牌的ActiveMQ
、RabbitMQ
,炙手可热的Kafka
,阿里巴巴自主开发RocketMQ
等。
2. 不同 MQ 特点
-
ActiveMQ
ActiveMQ 是 Apache 出品,最流行的,能力强劲的开源消息总线。它是一个完全支持 JMS 规范的的消息中间件。丰富的 API,多种集群架构模式让 ActiveMQ 在业界成为老牌的消息中间件,在中小型企业颇受欢迎!
-
Kafka
Kafka 是 LinkedIn 开源的分布式发布-订阅消息系统,目前归属于 Apache 顶级项目。Kafka 主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8 版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
-
RocketMQ
RocketMQ是阿里开源的消息中间件,它是纯 Java 开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ 思路起源于 Kafka,但并不是 Kafka 的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog 分发等场景。
-
RabbitMQ
RabbitMQ 是使用 Erlang 语言开发的开源消息队列系统,基于 AMQP 协议来实现。AMQP 的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。 AMQP 协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
3. RabbitMQ 安装
官网:https://www.rabbitmq.com/
下载界面:https://www.rabbitmq.com/download.html
根据官网提示,首先执行:
## Uses a PackageCloud-provided Yum repository setup script.
## Always verify what is downloaded before piping it to a privileged shell!
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash
## primary RabbitMQ signing key
rpm --import https://github.com/rabbitmq/signing-keys/releases/download/2.0/rabbitmq-release-signing-key.asc
## modern Erlang repository
rpm --import https://packagecloud.io/rabbitmq/erlang/gpgkey
## RabbitMQ server repository
rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
然后在 /etc/yum.repos.d/
目录下新增 rabbitmq.repo
文件:
cd /etc/yum.repos.d/
vim rabbitmq.repo
文件内容为:
# In /etc/yum.repos.d/rabbitmq.repo
##
## Zero dependency Erlang
##
[rabbitmq_erlang]
name=rabbitmq_erlang
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/$basearch
repo_gpgcheck=1
gpgcheck=1
enabled=1
# PackageCloud's repository key and RabbitMQ package signing key
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
https://github.com/rabbitmq/signing-keys/releases/download/2.0/rabbitmq-release-signing-key.asc
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[rabbitmq_erlang-source]
name=rabbitmq_erlang-source
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
# PackageCloud's repository key and RabbitMQ package signing key
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
##
## RabbitMQ server
##
[rabbitmq_server]
name=rabbitmq_server
baseurl=https://packagecloud.io/rabbitmq/rabbitmq-server/el/7/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[rabbitmq_server-source]
name=rabbitmq_server-source
baseurl=https://packagecloud.io/rabbitmq/rabbitmq-server/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
接着执行:
yum update -y
更新 Yum 包元数据。接下来就可以正式安装 package 了:
yum install -y erlang rabbitmq-server
此时已经安装完成,下面开始一些设置:
执行
systemctl enable rabbitmq-server.service
设置开机自启,然后启动服务:
systemctl start rabbitmq-server.service
查看服务状态:
systemctl status rabbitmq-server.service
如果要关闭服务,可以执行:
systemctl stop rabbitmq-server.service
RabbitMQ 自带了客户端,不过是网页版的,执行下面的命令开启:
rabbitmq-plugins enable rabbitmq_management
查看端口是否开放:
OK,全都开放了,我们就可以登录客户端看看了,输入网址:ip:15672
进入客户端
默认的用户名和密码都是 guest
,输入后登录:
可以看到,这个账户只允许本地登录,所以需要修改下连接权限以及新增账户。
rabbitmqctl add_user ice ice # 新增用户
rabbitmqctl set_permissions -p / ice "^ice-.*" ".*" ".*" # 设置权限
rabbitmqctl set_user_tags ice administrator # 设为管理账户
进入之后界面如下所示:
4. RabbitMQ 管理配置
4.1 RabbitMQ 管理命令行
# 1.服务启动相关
systemctl start|restart|stop|status rabbitmq-server
# 2.管理命令行 用来在不使用web管理界面情况下命令操作RabbitMQ
rabbitmqctl help 可以查看更多命令
# 3.插件管理命令行
rabbitmq-plugins enable|list|disable
4.2 web管理界面介绍
4.2.1 overview 概览
-
Connections:无论生产者还是消费者,都需要与 RabbitMQ 建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
-
Channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道
-
Exchanges:交换机,用来实现消息的路由
-
Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
4.2.2 Admin 用户和虚拟主机管理
1、添加用户
上面的Tags选项,其实是指定用户的角色,可选的有以下几个:
-
超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
-
监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
-
策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
-
普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
-
其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
2、创建虚拟主机
虚拟主机:为了让各个用户可以互不干扰的工作,RabbitMQ 添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。
3、绑定虚拟主机和用户
创建好虚拟主机,我们还要给用户添加访问权限:
点击添加好的虚拟主机:
进入虚拟机设置界面:
5. RabbitMQ 初步
5.1 AMQP 协议的回顾
5.2 RabbitMQ 支持的消息模型
5.3 引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.12.0</version>
</dependency>
5.4 新建虚拟主机
5.5 创建用户
然后配置访问权限:
然后选择允许访问的虚拟主机以及读写权限,这里默认读写都开启,只选择 /ems
主机:
此时新建的账户就可以访问新建的 /ems
虚拟主机:
5.4 第一种模型(直连)
在上图的模型中,有以下概念:
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接受者,会一直等待消息到来
- queue:消息队列,图中红色部分,类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息
1、开发生产者
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Provider {
// 生产消息
public static void testSendMessage() throws IOException, TimeoutException {
// 创建连接 mq 的连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接 RabbitMQ 的主机
connectionFactory.setHost("118.25.151.78");
// 设置端口号
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
// 获取连接对象
Connection connection = connectionFactory.newConnection();
// 获取连接中通道
Channel channel = connection.createChannel();
// 通道绑定对应消息队列
// 参数 1:queue,队列名称,没有会自动创建
// 参数 2:durable,定义队列是否要持久化,true 持久化队列,false 不持久化
// 参数 3:exclusive,是否独占队列,true 独占队列,false 不独占
// 参数 4:autoDelete,是否在消费完成后自动删除队列,true 自动删除,false 不自动删除
// 参数 5:额外附加参数
channel.queueDeclare("hello", false, false, false, null);
// 发布消息
// 参数 1:交换机名称
// 参数 2:队列名称
// 参数 3:传递消息额外设置
// 参数 4:消息的具体内容
channel.basicPublish("", "hello", null, "Hello RabbitMQ!".getBytes());
channel.close();
connection.close();
}
public static void main(String[] args) throws IOException, TimeoutException {
testSendMessage();
}
}
运行后,可以在管理界面看到:
2、开发消费者
import com.rabbitmq.client.*;
import org.junit.Test;
import javax.naming.ldap.ControlFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void testConsumeMessage() throws IOException, TimeoutException {
// 创建连接 mq 的连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 设置连接 RabbitMQ 的主机
connectionFactory.setHost("118.25.151.78");
// 设置端口号
connectionFactory.setPort(5672);
// 设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
// 获取连接对象
Connection connection = connectionFactory.newConnection();
// 获取连接中通道
Channel channel = connection.createChannel();
// 通道绑定对应消息队列
// 参数 1:queue,队列名称,没有会自动创建
// 参数 2:durable,定义队列是否要持久化,true 持久化队列,false 不持久化 (只针对队列,不是队列里的消息)
// 参数 3:exclusive,是否独占队列,true 独占队列,false 不独占
// 参数 4:autoDelete,是否在消费完成后自动删除队列,true 自动删除,false 不自动删除
// 参数 5:额外附加参数
channel.queueDeclare("hello", false, false, false, null);
// 消费消息
// 参数 1:消费哪个队列的消息
// 参数 2:开启消息的自动确认机制
// 参数 3: 消费时的回调接口
channel.basicConsume("hello", true, new DefaultConsumer(channel) {
@Override
// 最后一个参数是消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String content = new String(body);
System.out.println(content);
}
});
// 一般希望持续监听,不要关闭
// channel.close();
// connection.close();
}
public static void main(String[] args) throws IOException, TimeoutException {
testConsumeMessage();
}
}
执行消费者函数,可以看到控制台:
消费者端不建议关闭连接
3、RabbitMQ 连接工具类封装
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQUtil {
private static ConnectionFactory connectionFactory;
static {
// 静态代码块,类加载执行,只执行一次
connectionFactory = new ConnectionFactory();
connectionFactory.setHost("118.25.151.78");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/ems");
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
}
// 定义提供连接对象的方法
public static Connection getConnection() {
try {
return connectionFactory.newConnection();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
return null;
}
// 关闭通道和连接
public static void close(Channel channel, Connection connection) {
try {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
}
5.5 第二种模型(Work queues)
Work queues
,也被称为(Task queues
),任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用 work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
角色:
- P:生产者:任务的发布者
- C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
- C2:消费者-2:领取任务并完成任务,假设完成速度快
1、开发生产者
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare("work", true, false, false, null);
// 生产消息
for (int i = 0; i < 20; i++) {
channel.basicPublish("", "work", null, (i + "Hello work queue").getBytes());
}
// 关闭资源
RabbitMQUtil.close(channel, connection);
}
}
2、开发消费者-1
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
});
}
}
3、开发消费者-2
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer2 {
public static void main(String[] args) throws IOException {
// 获取连接
Connection connection = RabbitMQUtil.getConnection();
// 获取通道对象
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
});
}
}
4、测试结果
默认情况下,RabbitMQ 将按顺序将每个消息发送给下一个使用者,平均而言,每个消费者都会收到相同数量的消息,这种分发消息的方式称为循环。
5、消息自动确认机制
之前的案例,都开启了小子自动确认机制,这导致队列将消息发送给消费者后,会立即删除队列里的消息,万一消费者挂了,这些消息就消失了,没法处理,这是不被允许的。我们要对原来的代码做些改进:
-
设置通道一次只能消费一个消息
-
关闭消息的自动确认,开启手动确认消息
// 每次只能消费一个消息
channel.basicQos(1);
// 声明队列
channel.queueDeclare("work", true, false, false, null);
channel.basicConsume("work", false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new String(body));
// 手动确认,确认后队列才删除消息
// 参数 1:确认队列中哪个具体消息
// 参数 2:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
5.6 第三种模型(Publish/Subscribe)
在广播模式下,消息发送流程是这样的:
- 可以有多个消费者
- 每个消费者有自己的 queue(队列)
- 每个队列都要绑定到 Exchange(交换机)
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
- 交换机把消息发送给绑定过的所有队列
- 队列的消费者都能拿到消息,实现一条消息被多个消费者消费
1、开发生产者
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtil.getConnection();
Channel channel = connection.createChannel();
// 将通道声明指定交换机
// 参数 1:交换机名称
// 参数 2:交换机类型 fanout 代表广播类型
channel.exchangeDeclare("logs", "fanout");
// 发送消息
channel.basicPublish("logs", "", null, "fanout type message".getBytes());
// 释放资源
RabbitMQUtil.close(channel, connection);
}
}
2、开发消费者(多个,代码一样)
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtil.getConnection();
Channel channel = connection.createChannel();
// 通道绑定交换机
channel.exchangeDeclare("logs", "fanout");
// 临时队列
String queue = channel.queueDeclare().getQueue();
// 绑定交换机和队列
channel.queueBind(queue, "logs", "");
// 消费消息
channel.basicConsume(queue, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
});
}
}
5.7 第四种模型(Routing)
5.7.1 Routing 之订阅模型-Direct(直连)
在 Fanout 模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到 Direct 类型的 Exchange。
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 - Exchange 不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
流程:
图解:
- P:生产者,向 Exchange 发送消息,发送消息时,会指定一个 routing key
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与 routing key 完全匹配的队列
- C1:消费者,其所在队列指定了需要 routing key 为 error 的消息
- C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息
1、开发生产者
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
// 获取连接通道对象
Channel channel = connection.createChannel();
// 通过通道声明交换机
// 参数 1:交换机名称
// 参数 2:路由模式
channel.exchangeDeclare("logs_direct", "direct");
// 发送消息
String routingKey = "info";
channel.basicPublish("logs_direct", routingKey, null, ("这是direct模式发布的基于 route key:[" + routingKey + "]").getBytes());
RabbitMQUtil.close(channel, connection);
}
}
2、开发消费者-1
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Coustomer1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
// 获取连接通道对象
Channel channel = connection.createChannel();
// 通过通道声明交换机
// 参数 1:交换机名称
// 参数 2:路由模式
channel.exchangeDeclare("logs_direct", "direct");
// 创建一个临时队列
String queue = channel.queueDeclare().getQueue();
// 基于 route key 绑定队列和交换机
channel.queueBind(queue, "logs_direct", "error");
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者 1:" + new String(body));
}
});
}
}
3、开发消费者-2
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Coustomer2 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
// 获取连接通道对象
Channel channel = connection.createChannel();
// 通过通道声明交换机
// 参数 1:交换机名称
// 参数 2:路由模式
channel.exchangeDeclare("logs_direct", "direct");
// 创建一个临时队列
String queue = channel.queueDeclare().getQueue();
// 基于 route key 绑定队列和交换机
channel.queueBind(queue, "logs_direct", "info");
channel.queueBind(queue, "logs_direct", "error");
channel.queueBind(queue, "logs_direct", "warning");
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者 2:" + new String(body));
}
});
}
}
4、测试
5.7.2 Routing 之订阅模型-Topic
Topic
类型的 Exchange
与 Direct
相比,都是可以根据 RoutingKey
把消息路由到不同的队列。只不过 Topic
类型 Exchange
可以让队列在绑定 Routing key
的时候使用通配符!这种模型 Routingkey
一般都是由一个或多个单词组成,多个单词之间以 ”.” 分割,例如: item.insert
。
通配符:
- * (star) can substitute for exactly one word. 匹配不多不少恰好1个词
- # (hash) can substitute for zero or more words. 匹配一个或多个词
如:
audit.#
:匹配audit.irs.corporate
或者audit.irs
等audit.*
:只能匹配audit.irs
1、开发生产者
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
public class Provider {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机以及交换机类型 topic
channel.exchangeDeclare("topics", "topic");
// 发布消息
String routingKey = "user.save";
channel.basicPublish("topics", routingKey, null, ("这是topic动态路由模型,routingKey:[" + routingKey + "]").getBytes());
RabbitMQUtil.close(channel, connection);
}
}
2、开发消费者-1
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Coustomer1 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
// 获取连接通道对象
Channel channel = connection.createChannel();
// 通过通道声明交换机
// 参数 1:交换机名称
// 参数 2:路由模式
channel.exchangeDeclare("topics", "topic");
// 创建一个临时队列
String queue = channel.queueDeclare().getQueue();
// 绑定队列和交换机,动态通配符形式
channel.queueBind(queue, "topics", "user.*");
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者 1:" + new String(body));
}
});
}
}
3、开发消费者-2
import com.ice.util.RabbitMQUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Coustomer2 {
public static void main(String[] args) throws IOException {
// 获取连接对象
Connection connection = RabbitMQUtil.getConnection();
// 获取连接通道对象
Channel channel = connection.createChannel();
// 通过通道声明交换机
// 参数 1:交换机名称
// 参数 2:路由模式
channel.exchangeDeclare("topics", "topic");
// 创建一个临时队列
String queue = channel.queueDeclare().getQueue();
// 绑定队列和交换机,动态通配符形式
channel.queueBind(queue, "topics", "user.#");
channel.basicConsume(queue, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者 2:" + new String(body));
}
});
}
}
4、测试
6. SpringBoot 整合 RabbitMQ
6.1 搭建环境
1、引入依赖
其中 RabbitMQ 必需的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置 RabbitMQ
spring:
rabbitmq:
host: 118.25.151.78
port: 5672
username: ems
password: 123
virtual-host: /ems
RabbitTemplate
用来简化操作,使用时候直接在项目中注入即可使用
6.2 直连模型
生产者方法:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@DisplayName("直连模型")
public void test() {
rabbitTemplate.convertAndSend("hello","hello world");
}
}
消费者方法:
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RabbitListener(queuesToDeclare = @Queue(value = "hello",durable = "false",autoDelete = "true"))
public class HelloCustomer {
@RabbitHandler
public void receive(String message) {
System.out.println("message = " + message);
}
}
6.3 Work queues 模型
生产者方法:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@DisplayName("Work queues")
public void test() {
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("work", "work 模型");
}
}
}
消费者方法:
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class WorkCustomer {
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive1(String message) {
System.out.println("message1 = " + message);
}
@RabbitListener(queuesToDeclare = @Queue("work"))
public void receive2(String message) {
System.out.println("message2 = " + message);
}
}
说明:默认在 Spring AMQP 实现中 Work 这种方式就是公平调度,如果需要实现能者多劳需要额外配置
6.4 Fanout 广播模型(Publish/Subscribe)
生产者方法:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@DisplayName("Fanout")
public void test() {
rabbitTemplate.convertAndSend("logs","","Fanout 模型发送的消息");
}
}
消费者方法:
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class FanoutCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, // 创建临时队列
exchange = @Exchange(value = "logs", type = "fanout")
)
})
public void receive1(String message) {
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,
exchange = @Exchange(value = "logs", type = "fanout")
)
})
public void receive2(String message) {
System.out.println("message2 = " + message);
}
}
6.5 Routing 模型
生产者方法:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@DisplayName("Routing")
public void test() {
rabbitTemplate.convertAndSend("directs","error","Routing 模型发送的消息");
}
}
消费者方法:
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RouteCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, // 创建临时队列
exchange = @Exchange(value = "directs", type = "direct"), // 定义交换机名称和类型
key = {"info", "error", "warn"}
)
})
public void receive1(String message) {
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, // 创建临时队列
exchange = @Exchange(value = "directs", type = "direct"), // 定义交换机名称和类型
key = {"error"}
)
})
public void receive2(String message) {
System.out.println("message2 = " + message);
}
}
6.6 Topic 模型
生产者方法:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@DisplayName("Topic")
public void test() {
rabbitTemplate.convertAndSend("topics","order.save","Topic 模型发送的消息");
}
}
消费者方法:
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class TopicCustomer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, // 创建临时队列
exchange = @Exchange(value = "topics", type = "topic"), // 定义交换机名称和类型
key = {"user.save", "user.*"}
)
})
public void receive1(String message) {
System.out.println("message1 = " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue, // 创建临时队列
exchange = @Exchange(value = "topics", type = "topic"), // 定义交换机名称和类型
key = {"order.#", "produce.#", "user.*"}
)
})
public void receive2(String message) {
System.out.println("message2 = " + message);
}
}
7. RabbitMQ 的应用场景
7.1 异步处理
场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种 1.串行的方式 2.并行的方式
-
串行方式: 将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。这有一个问题是,邮件、短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西。
-
并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。
-
消息队列:假设三个业务节点分别使用 50 ms,串行方式使用时间 150 ms,并行使用时间 100 ms。虽然并行已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回。
引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理
由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间 + 写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的 3 倍,是并行的 2 倍。
7.2 应用解耦
场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口。
这种做法有一个缺点:
当库存系统出现故障时,订单就会失败,订单系统和库存系统高耦合。引入消息队列:
- 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
- 库存系统:订阅下单的消息,获取下单消息,进行库操作。就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。
7.3 流量削峰
场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。
作用 :
-
可以控制活动人数,超过此一定阀值的订单直接丢弃(我为什么秒杀一次都没有成功过呢^^)
-
可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)
-
用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面
-
秒杀业务根据消息队列中的请求信息,再做后续处理
8. RabbitMQ 集群
8.1 普通集群(副本集群)
默认情况下:RabbitMQ 代理操作所需的所有数据/状态都将跨所有节点复制,这方面的一个例外是消息队列,默认情况下,消息队列位于一个节点上,尽管它们可以从所有节点看到和访问
核心解决问题: 当集群中某一时刻 master 节点宕机,可以对 Queue 中信息进行备份
8.2 镜像集群
镜像队列机制就是将队列在三个节点之间设置主从关系,消息会在三个节点之间进行自动同步,且如果其中一个节点不可用,并不会导致消息丢失或服务不可用的情况,提升MQ集群的整体高可用性。