目录
1. 为什么很多公司都选择RabbitMQ?
2. RabbitMQ的高性能是如何实现的?
3. AMQP高级协议&核心概念
4. RabbitMQ整体架构是怎样的?
5. RabbitMQ中的消息是如何流转?
6. 安装&使用
7. 消息的生产者&消费者
8. RabbitMQ独有的Exchange交换机
9. 队列(Queue)、绑定(Binding)、虚拟主机(Virtual Host)、消息(Message)
一、RabbitMQ是什么
主要是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,它主要是使用Erlang语言进行编写的,并且还基于AMQP协议。
优点:
与SpringAMQP完美结合,拥有丰富的API
集群模式相当丰富,提供表达式配置,HA模式,镜像队列模型。
二、AMQP协议
Advanced Message Queuing Protocol高级消息协议。
三、RabbitMQ架构图
四、RabbitMQ消息流转
五、下载和安装
5.1 下载
5.1.1 下载Erlang的rpm包
RabbitMq是Erlang语言编写的,所以Erlang环境必须要有,需要注意的是:Erlang环境一定要与RabbitMq版本匹配
Erlang下载地址:(根据自身需求及版本关系,下载对应的rpm包)
5.1.2 下载socat的rpm包
rabbitmq安装依赖于socat,所以需要下载socat。
5.1.3下载RabbitMQ的rpm包
RabbitMQ下载地址:(根据自身需求及匹配关系,下载对应rpm包)
5.2 安装
5.2.1 安装Erlang、Socat、RabbitMQ
①rpm -ivh erlang-21.3.8.9-1.el7.x86_64.rpm
②rpm -ivh socat-1.7.3.2-1.el6.lux.x86_64.rpm
在安装rabbitmq之前需要先安装socat,否则,报错。
可以采用yum安装方式:yum install socat,我们这里采用rpm安装方式
③rpm -ivh rabbitmq-server-3.8.1-1.el7.noarch.rpm
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.1/sbin
5.2.2 启用管理插件
[jg@HadoopNode01 opt]$ sudo rabbitmq-plugins enable rabbitmq_management
5.2.3 启动RabbitMQ
systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
systemctl restart rabbitmq-server.service
systemctl stop rabbitmq-server.service
5.2.4 查看进程
ps -ef | grep rabbitmq
5.2.5 测试
-
关闭防火墙:systemctl stop firewalld
-
在Web浏览器输入地址:http://虚拟机ip:15672
-
输入默认账号密码:guest:guest(guest用户默认不允许远程连接)
-
增加自定义账号
-
添加管理员账号:rabbitmqctl add_user admin admin
-
分配账号角色:
rabbitmqctl set_user_tags admin administrator -
修改密码:rabbitmqctl change_password admin 123456
-
查看用户列表:rabbitmqctl list_users
-
管理界面标签页介绍
- overview:概览
- connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以直接查看连接情况
- channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。
- Exchanges:交换机,用来实现消息的路由
- Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。
- 端口:
- 5672:rabbitMq的编程语言客户端连接端口
- 15672:rabbitMq管理界面端口
- 25672:rabbitMq集群的端口
5.2.6 卸载
- rpm -qa | grep rabbitmq
- rpm -e rabbitmq-server
六、五种消息模型
RabbitMq提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不需要太多关注,那么也就剩下五种。但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
6.1 准备工作
6.1.1 创建project或者module
6.1.2 导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
6.1.3 建立连接工具类
public class ConnectionUtil {
public static Connection connection = null;
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("192.168.170.132");
//设置端口号
factory.setPort(5672);
//设置账号信息,用户名,密码,vhost
factory.setVirtualHost("/jg");
factory.setUsername("jg");
factory.setPassword("123456");
//通过工厂获取连接
connection = factory.newConnection();
return connection;
}
public static void closeConnection() throws Exception {
if (connection != null) {
connection.close();
}
}
//测试连接
public static void main(String[] args) throws Exception {
Connection con = ConnectionUtil.getConnection();
System.out.println(con);
con.close();
}
}
6.2 基本消息类型
RabbitMQ是一个消息代理:它接收和转发消息。你可以把它想象成一个邮局:当你把邮件放在邮箱里时,你可以确定邮差先生最终把邮件发送给你的收件人。
在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。
RabbitMQ与邮局的主要区别就是它不处理信息,而是接收,存储和转发数据消息的二进制数据块。
- P(producer/ publisher):生产者,一个发送消息的用户应用程序。
- C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
- 队列(红色区域):rabbitmq内部类似于邮箱的一个概念。
- 虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。
- 队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。
- 许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
总之:
- 生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。
- 我们将用Java编写两个程序;发送单个消息的生产者,以及接收消息并将其打印出来的消费者。
- 我们将详细介绍Java API中的一些细节,这是一个消息传递的“Hello World”。
- 我们将调用我们的消息发布者(发送者)Send和我们的消息消费者(接收者)Recv。
- 发布者将连接到RabbitMQ,发送一条消息,然后退出。
6.2.1 生产者发送消息
public class Send {
//定义要发送的队列
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception{
//获取连接
Connection connection = ConnectionUtil.getConnection();
//从连接中创建通道
Channel channel = connection.createChannel();
//声明/创建队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//消息内容
String message = "Hello World";
//向指定的队列中发送消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//管理通道和连接
channel.close();
ConnectionUtil.closeConnection();
}
}
6.2.2 消费者接收消息
public class Receive {
//声明要接收消息的队列
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception{
//获取连接
Connection connection = ConnectionUtil.getConnection();
//从连接中创建通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String (delivery.getBody(),"UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,consumerTag -> {});
}
}
6.2.3 消息确认机制(ACK)
- 通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。
- 那么问题来了:RabbitMQ怎么知道消息被接收了呢?
- 如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!
- 因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。
- 不过这种回执ACK分两种情况:
- 自动ACK:消息一旦被接收,消费者自动发送ACK
- 手动ACK:消息接收后,不会发送ACK,需要手动调用。
- 具体怎么选择要看消息的重要性
- 如果消息不太重要,丢失也没有影响,那么自动ACk会比较方便
- 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
- 我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码:
public class Receive2 {
//声明要接收消息的队列
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception{
//获取连接
Connection connection = ConnectionUtil.getConnection();
//从连接中创建通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
/**
* 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
* 即使已经完成了队列中信息的消费,程序还是不会停止,一直在监听队列中是否有新的消息。
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//body即消息体
String message = new String(body);
System.out.println(" [x] received : " + message + "!");
//手动进行ACK multiple:是否同时确认多个消息
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
/**
* @param autoAck 是否自动进行消息确认
*/
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
6.3 work模型
工作队列或者竞争消费者模式
在使用基本消息模型的时候,是从一个命名队列中发送和接收消息的。在这里我们将创建一个工作队列,在多个工作者之间分配耗时任务。
工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队。在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。
这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。
接下来我们来模拟这个流程:
- P:生产者:任务的发布者
- C1:消费者1:领取任务并且完成任务,假设完成速度较快
- C2:消费者2:领取任务并且完成任务,假设完成速度慢
面试题:避免消息堆积?
- 采用workqueue,多个消费者监听同一队列
- 接收到消息后,通过线程池异步消费
6.3.1 生产者
public class Send {
//定义队列
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws Exception {
//建立连接
Connection connection = ConnectionUtil.getConnection();
//从连接中建立通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//循环发布消息
for (int i = 0; i < 50; i++) {
//定义消息内容
String message = "task ......"+i;
//发布消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
//关闭通道和连接
channel.close();
ConnectionUtil.closeConnection();
}
}
6.3.2 消费者1
public class Receive {
//TODO:定义队列
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws Exception {
//TODO:建立连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接中获取通道
final Channel channel = connection.createChannel();
//TODO:声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//TODO:设置每个消费者同时只能消费一条消息
// channel.basicQos(1);
//TODO:定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者1] received :" + message + "!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动ACK
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//监听队列
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
6.3.3 消费者2
public class Receive2 {
//TODO:定义队列
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws Exception {
//TODO:建立连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接中获取通道
final Channel channel = connection.createChannel();
//TODO:声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//TODO:设置每个消费者同时只能消费一条消息
// channel.basicQos(1);
//TODO:定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者2] received :" + message + "!");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动ACK
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//监听队列
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
消费者1消费消息
消费者2消费消息
可以发现,两个消费者各自消费了25条消息,而且各不相同,这就实现了任务分发。
6.3.4 能者多劳
- 刚才的实现有问题吗?
消费者1比消费者2的效率要低,一次任务的耗时较长,然而两人最终消费的消息数量是一样的,消费者2大量时间处于空闲状态,消费者1一直忙碌。 - 现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多;
- 那么要怎么实现呢?
我们可以使用basicQos方法和prefetchCount=1设置,这告诉RabbitMQ一次不要向消费者发送多于一条消息,或者换句话说,不要向消费者发送新消息,直到它处理并确认了前一个消息。相反,它会将其分派给不是仍然忙碌的下一个消费者。
6.4 订阅模型分类
在之前的模式中,我们创建了一个工作队列。工作队列背后的假设是:每个任务只被传递给一个消费者。
在这一部分,我们将做一些完全不同的事情,我们将会传递一个消息给多个消费者。我们称这种模式为“发布-订阅”。
订阅模型示意图:
解读:
- 1个生产者,多个消费者
- 每个消费者都有自己的一个队列
- 生产者没有将消息直接发送到队列,而是发送到了交换机
- 每个队列都要绑定到交换机
- 生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者获取的目的。
- Exchange
交换机一方面接收生产者发送的消息,另一方面知道如何处理消息,例如递交给某个特别队列、递交给所有队列,或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有任何符合路由规则的队列,那么消息会丢失。
Exchange类型有以下几种:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式)的队列
6.5 订阅模型-Fanout
6.5.1 生产者
public class Send {
//TODO:定义exchange的名称
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] args) throws Exception{
//TODO:建立连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接中获取通道
final Channel channel = connection.createChannel();
//TODO:声明exchange,并指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//TODO:消息内容
String message = "Hello Fanout";
//TODO:发布消息到Exchange
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
//关闭通道释放连接
channel.close();
ConnectionUtil.closeConnection();
}
}
6.5.2 消费者1
public class Receive1 {
private final static String QUEUE_NAME = "fanout_exchange_queue_1";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] args) throws Exception{
//获取连接
Connection connection = ConnectionUtil.getConnection();
//从连接中获取通道
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
//定义队列消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者1] received : " + message + "!");
}
};
//监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
6.5.3 消费者2
public class Receive2 {
private final static String QUEUE_NAME = "fanout_exchange_queue_2";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] args) throws Exception{
//获取连接
Connection connection = ConnectionUtil.getConnection();
//从连接中获取通道
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
//定义队列消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者2] received : " + message + "!");
}
};
//监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
6.5.4 测试
我们应该先启动生产者,否则,先启动消费者时,由于要绑定交换机,此时,交换机不存在所以会报错。这就跟我们订阅微信公众号一样,如果公众号不存在的话,显然是不能订阅的。
6.6 订阅模型-Direct
- 有选择的接收消息。
- 在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
- 在路由模式中,我们将添加一个功能-我们将只能订阅一部分消息。
例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。 - 但是在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
- 在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
- P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给routing key完全匹配的队列
- C1:消费者,其所在队列指定了需要routing key为error的消息
- C2:消费者,其所在队列指定了需要routing key为info、error、waring的消息
6.6.1 生产者
public class Send {
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] args) throws Exception{
//TODO:获取连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接中获取通道
final Channel channel = connection.createChannel();
//TODO:声明通道,并指定通道的类型
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//TODO:消息内容
String message = "商品新增了,id=1001";
//TODO:发送消息,并且指定routing key为insert,代表新增商品
channel.basicPublish(EXCHANGE_NAME,"insert",null,message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
//关闭通道,释放连接
channel.close();
ConnectionUtil.closeConnection();
}
}
6.6.2 消费者1
public class Receive1 {
private final static String QUEUE_NAME = "direct_exchange_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] args) throws Exception{
//TODO:获取连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接中获取通道
final Channel channel = connection.createChannel();
//TODO:声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//TODO:绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
//TODO:定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息体
String message = new String(body);
System.out.println(" [消费者1] received : " + message + "!");
}
};
//监听队列,自动ACK
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
6.6.3 消费者2
我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。
public class Receive2 {
private final static String QUEUE_NAME = "direct_exchange_queue_2";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] args) throws Exception{
//TODO:获取连接
Connection connection = ConnectionUtil.getConnection();
//TODO:根据连接获取通道
final Channel channel = connection.createChannel();
//TODO:声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//TODO:绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"insert");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
//TODO:定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者2] received : " + message + "!");
}
};
//TODO:监听队列,自动ACK
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
6.7 订阅模型-Topic
-
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing Key的时候使用通配符
-
RoutingKey一般都是由一个或者多个单词组成,多个单词之间以"."分割,例如:item.insert
-
通配符规则:
- ‘#’:匹配0或多个词(含零个)
- ‘*’:匹配不多不少恰好一个词(不含零个)
-
举例:
- ‘audit#’:能够匹配’audit.irs.corporate’或者’auditirs’
- ‘audit*’:只能匹配’audit.irs’
-
在这个例子中,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:".."。
-
我们创建三个绑定:Q1绑定了绑定键"* .orange.* “,Q2绑定了”* .*rabbit"和"lazy.#"。
-
Q1匹配所有的橙色动物。
-
Q2匹配关于兔子以及懒惰动物的消息。
-
练习,生产者发送如下消息,会进入那个队列:
- quick.orange.rabbit
- lazy.orange.elephant
- quick.orange.fox
- lazy.pink.rabbit
- quick.brown.fox
- quick.orange.male.rabbit
- orange
6.7.1 生产者
使用topic类型的Exchange,发送消息的routing key有三种:item.insert、item.update、item.delete。
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] args) throws Exception {
//TODO:获取连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接中获取通道
Channel channel = connection.createChannel();
//TODO:声明exchange并指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//TODO:指定要发送消息的内容
String message = "新增商品:id=1001";
//TODO:发送消息
channel.basicPublish(EXCHANGE_NAME,"item.insert",null,message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
//TODO:关闭通道,释放连接
channel.close();
ConnectionUtil.closeConnection();
}
}
6.7.2 消费者1
public class Receive1 {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] args) throws Exception {
//TODO:获取连接
Connection connection = ConnectionUtil.getConnection();
//TODO:从连接种获取通道
Channel channel = connection.createChannel();
//TODO:声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//TODO:绑定队列到交换机上,同时指定需要订阅的routing key
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"item.delete");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"item.update");
//TODO:定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者1] received : " + message + "!");
}
};
//TODO:监听队列,自动ACK
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
6.7.3 消费者2
public class Receive2 {
private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] args) throws Exception{
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"item.*");
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println(" [消费者2] received : " + message + "!");
}
};
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
6.8 持久化
- 如何避免消息丢失?
- 消费者的ACK机制。可以防止消费者丢失消息
- 但是,如果在消费者消费之前,MQ就宕机了,消息就没了
- 如何将消息进行持久化?
- 要将消息持久化,前提是:队列、Exchange都持久化
6.8.1 交换机持久化和消息持久化
6.8.2 队列持久化
七、Spring AMQP
7.1 简介
Spring有很多不同的项目,其中就有对AMQP的支持:
SpringAMQP的页面
Spring AMQP是对AMQP协议的抽象实现,而spring-rabbit是对协议的具体实现,也就是目前的唯一实现。底层使用的就是RabbitMQ。
7.2 依赖和配置
添加AMQP的启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在application.yml中添加RabbitMQ地址
spring:
rabbitmq:
host: 192.168.170.132
username: jg
password: 123456
virtual-host: /jg
port: 5672
7.3 监听者
在Spring AMOP中,对消息的消费者进行了封装和抽象,一个普通的JavaBean中的普通方法,只要通过简单的注解,就可以成为一个消费者。
@Component
public class Listener {
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "spring.test.queue", durable = "true"),
exchange = @Exchange(value = "spring.test.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC),
key = {"#.#"}))
public void listen(String msg) {
System.out.println("接收到消息:" + msg);
}
}
-
@Componet:类上的注解,注册到Spring容器
-
@RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:
- bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:
- value:这个消费者关联的队列。值是@Queue,代表一个队列
- exchange:队列所绑定的交换机,值是@Exchange类型
- key:队列和交换机绑定的RoutingKey
类似listen这样的方法在一个类中可以写多个,就代表多个消费者。
7.4 AmqpTemplate
Spring最擅长的事情就是封装,把他人的框架进行封装和整合。
Spring为AMQP提供了统一的消息处理模板:AmqpTemplate,非常方便的发送消息,其发送方法:
7.5 测试代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringAmqpDemoApplication.class)
class SpringAmqpDemoApplicationTests {
@Autowired
private AmqpTemplate amqpTemplate;
@Test
void testSend() throws Exception{
String message = "hello,spring amqp";
amqpTemplate.convertAndSend("spring.test.exchange","a.b",message);
//等待10秒后结束
Thread.sleep(10000);
}
}
结果