什么是MQ?
MQ全称为Message Queue,即消息队列。“消息队列”是在消息的传输过程中保存消息的容器。也是在消息的传输过程中保存消息的容器。用于分布式系统之间进行通信。
典型的:生产者、消费者模型。
MQ本身就相当于一个中间件,用于生产方通过MQ与消费方之间的交互作用。
RabbitMQ应用场景
场景一 、解耦
上面我们提到MQ本身就相当于一个中间件,用于生产方通过MQ与消费方之间的交互作用。让我们看一下传统模式和这种中间件模式的对比:
传统模式:
系统间耦合性太强,如图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!
中间件模式:
将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。
场景二 、异步
传统模式:
一些非必要的业务逻辑以同步的方式运行,太耗费时间
中间件模式:
将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度
场景三、削峰
传统模式:
并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常
中间件模式:
系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息
RabbitMQ的使用
RabbitMQ是一个消息代理:它接受和转发消息。 你可以把它想象成一个邮政信箱
RabbitMQ与邮局的主要区别是它不处理纸张,而是接受,存储和转发数据消息的二进制数据块。
如何避免消息堆积?
1) 采用workqueue,多个消费者监听同一队列。
2)接收到消息以后,而是通过线程池,异步消费。
如何避免消息丢失?
1) 消费者的ACK机制。可以防止消费者丢失消息。
但是,如果在消费者消费之前,MQ就宕机了,消息就没了?
2)可以将消息进行持久化。要将消息持久化,前提是:队列、Exchange都持久化
RabbitMQ提供了6种消息模型,但是第6种是RPC,并不是MQ,因此不学习。那么也就剩下5种,3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
1.Simple-简单模型
P(producer/ publisher):生产者,一个发送消息的用户应用程序。
C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
引入依赖
我们将用Java编写两个程序;发送单个消息的生产者,以及接收消息并将其打印出来的消费者。我们将详细介绍Java API中的一些细节,这是一个消息传递的“Hello World”。我们将调用我们的消息发布者(发送者)Send和我们的消息消费者(接收者)Recv。发布者将连接到RabbitMQ,发送一条消息,然后退出。
新建一个spring boot项目,使用spring 2和jdk 8,引入下面的依赖
<!--引入rabbitmq依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>3.1.5</version>
</dependency>
新建一个连接工具类:
package com.test.demo.util;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* rabbitMQ连接类
*
* @author jg
* @since 2023/11/7 15:27
*/
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
*
* @return Connection
* @throws Exception 异常
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("你的IP地址");
//端口,rabbitmq默认端口号为5672,阿里云等服务器记得开启防火墙和安全组
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setUsername("username");
factory.setPassword("password");
//虚拟主机路径,不同虚拟主机之间的数据不共享
factory.setVirtualHost("/");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
生产者
创建一个send类,充当简单模型生产者来发送消息
package com.test.demo.util.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.test.demo.util.ConnectionUtil;
/**
* 简单模式---生产者
*
* @author jg
* @since 2023/11/7 15:33
*/
public class Send {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 轻量级的 Connection,这是完成大部分API的地方。
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();
connection.close();
}
}
运行之后控制台会出现:
这时候我们打开rabbitMQ的web页面,登陆上去查看队列中的信息:
这就是我们刚才发的消息,在页面的控制台查看消息是不会进行消费的,所以消息还在
消费者
创建消费者来获取消息
package com.test.demo.util.simple;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 简单模型---消费者
* 自动ACK:消息一旦被接收,消费者自动发送ACK
*
* @author jg
* @since 2023/11/7 15:56
*/
public class Recv {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.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) throws IOException {
// body 即消息体
String msg = new String(body);
//添加异常,测试自动ACK
//int i = 1 / 0;
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
运行之后控制台会出现:
这时,我们再点开rabbitmq的web页面就会发现刚才发的消息没有了
因为我们并没有关闭消费者的连接,所以消费者是一直可以接收生产者发送的消息,每调用一次生产者Send发送消息,消费者Recv的控制台都会出现一条信息
消息确认机制(ACK)
通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。
那么问题来了:RabbitMQ怎么知道消息被接收了呢?
如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:
- 自动ACK:消息一旦被接收,消费者自动发送ACK
- 手动ACK:消息接收后,不会发送ACK,需要手动调用
选择哪种ACK主要看消息的重要性:
- 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
- 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
测试自动ACK
还是拿简单模式举例:
在简单模式的消费者Recv中添加代码,引发异常:
注意最后一行代码,第二个参数为true时,就会自动进行ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
修改完成之后我们重启消费者Recv,然后启动生产者发送一条信息
可以看到控制台报错,但是rabbitMQ的队列中消息依旧被消费了
测试手动ACK
我们注释掉引发报错的代码,并且把basicConsume方法第二个参数改为false(手动ACK)
先重启Recv然后用生产者发送一条信息
可以看到这里有一条未进行ACK消息确认的信息.
然后我们把Recv消费者停掉,控制台就会出现以下变化
这是因为我们没有对消息进行ACK,所以消息没有被消费掉,在消费者宕机之后,消息就会再次进入ready状态
现在我们修改代码,进行手动ACK
/**
* 手动进行ACK
* deliveryTag:该消息的index
* multiple:是否批量处理.true:将一次性ack所有消息。
*/
channel.basicAck(envelope.getDeliveryTag(), false);
改完代码重新启动消费者Recv,打开控制台就会发现消息已经没了:
消息手动ACK成功!
2.Work-工作模型
工作队列或者竞争消费者模式
工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。
这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。
假设现在有以下场景:
P:生产者:任务的发布者
C1:消费者,领取任务并且完成任务,假设完成速度较快
C2:消费者2:领取任务并完成任务,假设完成速度慢
生产者
跟简单模型基本一样,不过这里是循环发送50条消息。
package com.test.demo.util.work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.test.demo.util.ConnectionUtil;
/**
* work模型---生产者
* 工作模型:一个生产者对应多个消费者,一个消息只能被一个消费者消费
* @author jg
* @since 2023/11/7 16:30
*/
public class Send {
private final static String QUEUE_NAME = "work_queue";
public static void main(String[] argv) 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 + "'");
Thread.sleep(i * 2);
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
消费者1
package com.test.demo.util.work;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 工作模型消费者1
* @author jg
* @since 2023/11/7 16:32
*/
public class Recver1 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String QUEUE_NAME = "work_queue";
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 {
String msg = new String(body);
System.out.println("Recver1:" + msg);
channel.basicAck(envelope.getDeliveryTag(),false);
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
}
};
channel.basicConsume(QUEUE_NAME,false, consumer);
}
}
消费者2
package com.test.demo.util.work;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 工作模型消费者2
*
* @author jg
* @since 2023/11/7 16:33
*/
public class Recver2 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
String QUEUE_NAME = "work_queue";
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 {
String msg = new String(body);
System.out.println("Recver2:" + msg);
channel.basicAck(envelope.getDeliveryTag(),false);
}catch (Exception e){
e.printStackTrace();
}
}
};
channel.basicConsume(QUEUE_NAME,false, consumer);
}
}
消费者2与消费者1基本类似,就是没有设置消费耗时时间。
这里是模拟有些消费者快,有些比较慢。
接下来,两个消费者一同启动,然后发送50条消息:
可以发现,两个消费者各自消费了25条消息,而且各不相同,这就实现了任务的分发。
刚才的实现有问题吗?
消费者1比消费者2的效率要低,一次任务的耗时较长
然而两人最终消费的消息数量是一样的
消费者2大量时间处于空闲状态,消费者1一直忙碌
现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。
怎么实现呢?
我们可以使用basicQos方法和prefetchCount = 1设置。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是仍然忙碌的下一个工作人员。
两个消费者对象中都需添加:
//设置每个消费者同时只能处理一条信息
channel.basicQos(1);
改完之后再次测试就会发现消费者2处理了47条信息,消费者1实现了3条信息:
这就实现能者多劳了.
3.Fanout-广播模型
在广播模式下,消息发送流程是这样的:
1) 可以有多个消费者
2) 每个消费者有自己的queue(队列)
3) 每个队列都要绑定到Exchange(交换机)
4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
5) 交换机把消息发送给绑定过的所有队列
6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
生产者
跟简单模型和工作模型相比有两个变化:
- 1) 声明Exchange,不再声明Queue
- 2) 发送消息到Exchange,不再发送到Queue
package com.test.demo.util.fanout;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.test.demo.util.ConnectionUtil;
/**
* 广播模式--生产者
* 1) 声明Exchange,不再声明Queue
* 2) 发送消息到Exchange,不再发送到Queue
*
* @author jg
* @since 2023/11/8 10:35
*/
public class Send {
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为fanout
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "Hello everyone";
// 发布消息到Exchange
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [生产者] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1
package com.test.demo.util.fanout;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 广播模式--消费者1
* 代码中:队列需要和交换机绑定
* @author jg
* @since 2023/11/8 10:36
*/
public class Recv {
private final static String QUEUE_NAME = "fanout_exchange_queue_1";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
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 {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2
package com.test.demo.util.fanout;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 广播模式--消费者2
* 代码中:队列需要和交换机绑定
* @author jg
* @since 2023/11/8 10:36
*/
public class Recv2 {
private final static String QUEUE_NAME = "fanout_exchange_queue_2";
private final static String EXCHANGE_NAME = "fanout_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
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 {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动返回完成
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
因为我们这里是先声明交换机,然后再声明队列,所以我们要先启动生产者Send,然后再启动两个消费者,否则会报错
测试结果
启动完成后我们重新运行生产者Send发送一条信息,可以看到两个消费者的控制台出现:
这就说明广播模式创建成功了
广播模式是将消息发送给交换机下所有的队列,不用选择,所以速度也是最快的
4.Direct-定向模型
- 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、warning 的消息
生产者
package com.test.demo.util.direct;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.test.demo.util.ConnectionUtil;
/**
* 路由(定向)模式--生产者
*
* @author jg
* @since 2023/11/8 13:51
*/
public class Send {
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 消息内容
String message = "商品新增了, id = 1001";
String message2 = "商品修改了, id = 1002";
String message3 = "商品删除了, id = 1003";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
channel.basicPublish(EXCHANGE_NAME, "update", null, message2.getBytes());
channel.basicPublish(EXCHANGE_NAME, "delete", null, message3.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1
package com.test.demo.util.direct;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 定向模式--消费者1
*
* @author jg
* @since 2023/11/8 13:52
*/
public class Recv {
private final static String QUEUE_NAME = "direct_exchange_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2
package com.test.demo.util.direct;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* 定向模式--消费者2
*
* @author jg
* @since 2023/11/8 13:52
*/
public class Recv2 {
private final static String QUEUE_NAME = "direct_exchange_queue_2";
private final static String EXCHANGE_NAME = "direct_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的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");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
还是先启动Send来创建交换机Exchange,然后再启动两个消费者
测试结果
我们分别发送增、删、改的RoutingKey,发现结果:
定向模型的路由是全匹配的,不能带.或者#等符号,否则会获取不到消息
5.Topic-主题模型
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
举例:
audit.#:能够匹配:audit.irs.corporate` 或者 `audit.irs
audit.*:只能匹配:audit.irs
*.audit.*:只能匹配:test.audit.irs
我们创建了两个绑定:Q1绑定了绑定键“*.orange.*”,Q2绑定了“*.*.rabbit”和“lazy.#”。
Q1匹配所有的橙色动物。
Q2匹配关于兔子以及懒惰动物的消息。
生产者发送如下消息,会进入哪个队列?
- quick.orange.rabbit--Q1
- lazy.orange.elephant--Q2
- quick.orange.fox--Q1
- lazy.pink.rabbit--Q2
- quick.brown.fox--无
- quick.orange.male.rabbit--无
- orange--无
使用topic类型的Exchange,发送消息的routing key有3种: item.isnert
、item.update
、item.delete
:
生产者
package com.test.demo.util.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import com.test.demo.util.ConnectionUtil;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* Topic主题模型--生产者
*
* @author jg
* @since 2023/11/8 14:03
*/
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "新增商品 : id = 1001";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者1
package com.test.demo.util.topic;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* Topic主题模型--消费者
*
* @author 蒋果
* @since 2023/11/8 14:04
*/
public class Recv {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2
package com.test.demo.util.topic;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Topic主题模型--消费者2
*
* @author 蒋果
* @since 2023/11/8 14:04
*/
/**
* 消费者2
*/
public class Recv2 {
private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "lazy.#");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
channel.basicAck(envelope.getDeliveryTag(), false);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
还是一样,先启动Send生产者创建交换机,然后再启动消费者,最后再重启生产者发送信息
测试结果
只有消费者2接收到消息,说明主题模式搭建成功!
持久化
如何避免消息丢失?
1) 消费者的手动ACK机制。可以防止业务处理失败。
2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。
是可以将消息进行持久化呢?
要将消息持久化,前提是:队列、Exchange都持久化
我们再前面的例子中创建了很多的交换机和队列:
交换机:
队列:
现在我们重启下rabbitMQ服务:
重启后,我们之前创建的交换机和队列都消失了
正是因为这样,所以我们才需要持久化.
1.交换机持久化
修改完成后启动,然后查看rabbitmq的交换机控制台
出现这个标记,就表示这个交换机是持久化状态
亲测,重启后,这个交换机还在
2.队列持久化
运行后查看队列控制台:
队列持久化成功!
3.消息持久化
非持久化:
在做非持久化测试时,消费者不能设置自动ACK,要手动ACK,因为设置了线程睡眠rabbitmq的队列控制台不会显示消息数量.
生产者
package com.test.demo.util.topic;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import com.test.demo.util.ConnectionUtil;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* Topic主题模型--生产者
*
* @author jg
* @since 2023/11/8 14:03
*/
public class Send {
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明exchange,指定类型为topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic",true);
// 消息内容
String message = "消息 : 发送";
// 发送消息,并且指定routing key 为:insert ,代表新增商品
for (int i = 0; i < 50; i++) {
channel.basicPublish(EXCHANGE_NAME, "quick.orange.rabbit", null, ("quick.orange.rabbit"+i).getBytes());
}
System.out.println(" [商品服务:] Sent '" + message + "'");
channel.close();
connection.close();
}
}
消费者
package com.test.demo.util.topic;
import com.rabbitmq.client.*;
import com.test.demo.util.ConnectionUtil;
import java.io.IOException;
/**
* Topic主题模型--消费者
*
* @author jg
* @since 2023/11/8 14:04
*/
public class Recv {
private final static String QUEUE_NAME = "topic_exchange_queue_1";
private final static String EXCHANGE_NAME = "topic_exchange_test";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.orange.*");
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
try {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
// 监听队列,自动ACK
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
可以看到消费者设置了,每接收一条信息就暂停2秒钟
Thread.sleep(2000);
发送50条信息后,在消费者还没接受完的时候,我们重启rabbitmq服务器,看看消息还在不在
消费者接收
控制台:
重启服务器:
可以看到,正在处理消息时,rabbitmq服务器重启,没有设置持久化的消息会全部丢失
持久化:
在发送消息时在basicPublish方法中配置这个参数:
channel.basicPublish(EXCHANGE_NAME, "quick.orange.rabbit", MessageProperties.PERSISTENT_TEXT_PLAIN, ("quick.orange.rabbit"+i).getBytes());
然后发送消息,在Recv消费者处理消息的时候,重启rabbitmq服务器
重启时:
重启后:
可以看到消息恢复了,并且消费者还在继续处理消息
消息持久化成功!
使用RabbitTemplate
RabbitAdmin类
RabbitAdmin类是Spring AMQP提供的一个辅助类,用于简化与RabbitMQ交互的操作。它提供了一些便捷的方法,可以用于声明和管理队列、交换机和绑定等。
RabbitAdmin类的主要作用有以下几点:
- 声明和删除队列:RabbitAdmin类提供了
declareQueue()
和deleteQueue()
等方法,可以用于声明和删除队列。你可以通过这些方法来创建和销毁队列,设置队列的属性和参数。 - 声明和删除交换机:RabbitAdmin类提供了
declareExchange()
和deleteExchange()
等方法,用于声明和删除交换机。你可以通过这些方法来创建不同类型的交换机,设置交换机的属性和参数。 - 声明和绑定队列和交换机:RabbitAdmin类提供了
declareBinding()
和removeBinding()
等方法,可以用于声明和删除队列和交换机之间的绑定。你可以使用这些方法来将队列绑定到交换机上,设置绑定的选项。 - 管理RabbitMQ组件:RabbitAdmin类还提供了其他一些方法,如
declareExchange()
、declareBinding()
等,用于管理RabbitMQ的其他组件,如连接、通道、消费者等。
通过使用RabbitAdmin类,我们可以通过简单的方法调用来完成与RabbitMQ的操作,而不需要手动编写复杂的AMQP协议代码。它提供了一种方便和易于使用的方式来管理RabbitMQ的各种组件,简化了与RabbitMQ的交互过程。
RabbitTemplate类
RabbitTemplate类是Spring AMQP提供的一个核心类,用于在应用程序中与RabbitMQ进行交互。它封装了与RabbitMQ的连接、通道和基本操作,提供了一组方法来发送和接收消息,以及处理消息发送和接收的相关逻辑。
RabbitTemplate类的主要作用有以下几点:
- 消息发送:RabbitTemplate类提供了多个发送消息的方法,例如
convertAndSend()
、send()
等。你可以使用这些方法将消息发送到指定的交换机和队列,并指定路由键、消息体和其他参数。 - 同步和异步发送:RabbitTemplate类支持同步和异步的消息发送方式。你可以选择使用同步方式发送消息,等待发送结果返回;也可以选择使用异步方式发送消息,通过回调函数处理发送结果。
- 消息接收:RabbitTemplate类提供了多个接收消息的方法,例如
receiveAndConvert()
、receive()
等。你可以使用这些方法从指定的队列中接收消息,并指定超时时间、消息转换器和其他参数。 - 消息转换:RabbitTemplate类支持消息的自动转换,可以将Java对象自动序列化为消息体,并在接收时将消息体自动反序列化为Java对象。你可以通过设置消息转换器(MessageConverter)来定制消息的序列化和反序列化过程。
- 消息回复:RabbitTemplate类支持发送消息后等待并接收消息的模式,用于实现请求-响应模式。你可以在发送请求时指定回调队列,并在接收到响应消息时进行处理。
通过使用RabbitTemplate类,我们可以方便地在应用程序中与RabbitMQ进行消息的发送和接收,而不需要手动处理底层的连接、通道和协议细节。它提供了一种高层次的抽象,简化了与RabbitMQ的交互过程,并提供了一些便捷的方法和功能来处理消息发送和接收的相关逻辑。
使用示例
1.配置部分
server:
port: 8080
spring:
application:
name: rabbitMQ-Demo
#rabbitmq连接配置
rabbitmq:
host: 你的IP地址
port: 5672
username: admin
password: 123456
virtual-host: /
2.代码部分
package com.test.demo.util;
import cn.hutool.json.JSONObject;
import lombok.AllArgsConstructor;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* RabbitTemplate测试类
*
* @author 蒋果
* @since 2023/11/8 15:38
*/
@AllArgsConstructor
@RestController
@RequestMapping("/demo")
public class rabbitMQController {
private final RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public String send(@RequestParam("message") String message) {
//创建管理器
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitTemplate.getConnectionFactory());
//声明交换机
rabbitAdmin.declareExchange(new DirectExchange("TopicExchange", true, false));
//声明队列
rabbitAdmin.declareQueue(new Queue("demo.Topic", true));
//声明绑定
rabbitAdmin.declareBinding(
BindingBuilder
.bind(QueueBuilder.durable("demo.Topic").build())
.to(new DirectExchange("TopicExchange", true, false))
.with("msg")
);
//获取队列信息,名称,消息数量,消费者数量
rabbitAdmin.getQueueInfo("demo.Topic");
JSONObject msg = new JSONObject();
msg.put("msg", message);
msg.put("msg2", "31212312312");
String s = msg.toString();
rabbitTemplate.convertAndSend("DirectExchange", "msg", s);
return message;
}
}
RabbitAdmin类主要用于创建交换机,队列还有绑定交换机
RabbitTemplate类主要用来发送和接收队列中的信息,可以把信息转换成各种对象
@RabbitListener注解
开启注解
使用这个注解需要在启动类加上@EnableRabbit注解:
//开启rabbit注解
@EnableRabbit
代码示例:
package com.test.demo.util;
import cn.hutool.json.JSONObject;
import lombok.AllArgsConstructor;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
/**
* rabbitmq监听器
*
* @author 蒋果
* @since 2023/11/9 11:11
*/
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "demoDirect", durable = "true"),
exchange = @Exchange(value = "DirectExchange", type = ExchangeTypes.DIRECT,declare = "true"),
key = "msg"
))
@AllArgsConstructor
public class MyMessageListener {
@RabbitHandler
public void ttlListerer(String msg){
JSONObject msg1 = msg == null ? null : new JSONObject(msg);
if (msg1 != null) {
System.out.println("收到消息:"+msg1.get("msg"));
}
}
}
一些报错的处理
1.接收消息时反序列化出错
Attempt to deserialize unauthorized class java.util.HashMap; add allowed class name patterns to the message converter or, if you trust the message orginiator, set environment variable 'SPRING_AMQP_DESERIALIZATION_TRUST_ALL' or system property 'spring.amqp.deserialization.trust.all' to true
这个错误提示的是Spring AMQP正在尝试反序列化HashMap类,但是由于安全原因,这个类并未被允许。在Spring Boot中,可以通过以下方式来配置,在启动类中添加jsonMessageConverter和classMapper方法,并注册为bean:
package com.test.demo;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.support.converter.DefaultClassMapper;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableRabbit
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public MessageConverter jsonMessageConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
converter.setClassMapper(classMapper());
return converter;
}
@Bean
public DefaultClassMapper classMapper() {
DefaultClassMapper classMapper = new DefaultClassMapper();
// 添加允许的类名模式
classMapper.setTrustedPackages("java.util.HashMap");
return classMapper;
}
}
2 条评论
这篇文章不错!
文章结构紧凑,层次分明,逻辑严密,让人一读即懂。