RabbitMQ常用的五种模式介绍
1、简单模式
简单模式:该模式是个一对一模式,只有一个生产者(用于生产消息),一个队列 Queue(用于存储消息),一个消费者 C (用于接收消息)。
注:简单模式也用到了交换机,使用的是默认的交换机(AMQP default)。
代码实现
[1] 创建一个Maven项目
- RabbitMQ:父工程
- rabbitmq-commons:存放工具类
- rabbitmq-consumer:消费者模块
- rabbitmq-producer:生产者模块
[2] 导入依赖
在父工程中的 pom.xml 文件导入如下依赖:
<!-- mq的依赖 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
[3] 封装工具类
在rabbitmq-commons模块中封装 rabbitmq 连接的工具类(注意:其它模块要使用工具类只需引入本模块即可!)
/**
* 封装连接工具类
*/
public class ConnectionUtils {
public static Connection getConnection() throws Exception {
// 1.定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2.设置服务器地址
factory.setHost("192.168.43.128");
// 3.设置协议端口号
factory.setPort(5672);
// 4.虚拟主机名称;默认为 /
factory.setVirtualHost("/");
// 5.设置用户名称
factory.setUsername("admin");
// 6.设置用户密码
factory.setPassword("123456");
// 7.创建连接
Connection connection = factory.newConnection();
return connection;
}
}
[4] 创建生产者
生产者负责创建消息并且将消息发送至指定的队列中,简单分为5步:
- 创建连接
- 创建通道
- 创建(声明)队列
- 发送消息
- 关闭资源
/**
* 生产者(简单模式)
*/
public class Producer {
// 队列名称
private static final String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception {
// 1、获取连接
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、声明(创建)队列
/*
* queue 参数1:声明通道中对应的队列名称
* durable 参数2:是否定义持久化队列,当mq重启之后队列还在
* exclusive 参数3:是否独占本次连接,为true则只能有一个消费者监听这个队列
* autoDelete 参数4:是否自动删除队列,如果为true表示没有消息也没有消费者连接自动删除队列
* arguments 参数5:队列其它参数(额外配置)
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 4.发送消息
/*
* exchange 参数1:交换机名称,如果没有指定则使用默认Default Exchange
* routingKey 参数2:队列名称或者routingKey,如果指定了交换机就是routingKey路由key,简单模式可以传递队列名称
* props 参数3:消息的配置信息
* body 参数4:要发送的消息内容
*/
String msg = "Hello RabbitMQ!!!";
System.out.println("生产者发送的消息:" + msg);
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
//关闭资源
channel.close();
connection.close();
}
}
[5] 创建消费者
消费者实现和生产者实现过程差不多,但是没有关闭通道和连接,因为消费者要一直等待随时可能发来的消息,大致分为如下3步:
- 获取连接
- 创建通道
- 监听队列,接收消息
/**
* 消费者(简单模式)
*/
public class Consumer {
// 队列名称
private static final String QUEUE_NAME = "simple_queue";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3. 创建队列Queue,如果没有一个名字叫simple_world的队列,则会创建该队列,如果有则不会创建.
// 这里可有可无,但是发送消息是必须得有该队列,否则消息会丢失
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 4、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
/*
* handleDelivery回调方法,当收到消息后,会自动执行该方法
* consumerTag 参数1:消费者标识
* envelope 参数2:可以获取一些信息,如交换机,路由key...
* properties 参数3:配置信息
* body 参数4:读取到的消息
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取消息:" + new String(body));
}
};
/*
* queue 参数1:队列名称
* autoAck 参数2:是否自动确认,true表示自动确认接收完消息以后会自动将消息从队列移除。否则需要手动ack消息
* callback 参数3:回调对象,在上面定义了
*/
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
[6] 运行结果
把生产者的代码运行三次,表示向队列中发送了三次消息。
查看RabbitMQ控制台中的内容。
最后启动消费者,查看控制台打印的数据。
简单模式的不足之处:这种模式是一对一,一个生产者向一个队列中发送消息,一个消费者从绑定的队列中获取消息,这样耦合性过高,如果有多个消费者想消费队列中信息就无法实现了。
2、工作模式
工作模式:也被称为任务模型(Task Queues)。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用 work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行。
这种模式只有一个生产者 P,一个用于存储消息的队列 Queue、多个消费者 C 用于接收消息。
工作队列模式的特点有三:
- 一个生产者,一个队列,多个消费者同时竞争消息
- 任务量过高时可以提高工作效率
- 消费者获得的消息是无序的
代码实现
[1] 创建生产者
向队列中发送10条消息。
/**
* 生产者(工作模式)
*/
public class Producer {
// 队列名称
private static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws Exception {
// 1、创建连接
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道
Channel channel = connection.createChannel();
// 3、声明队列 queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加属性参数)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 4、发送10条消息
for (int i = 1; i <= 10; i++) {
String msg = "Hello RabbitMQ!!!~~~" + i;
System.out.println("生产者发送消息:" + msg);
// basicPublish(交换机名称-""表示不用交换机,队列名称或者routingKey, 消息的属性信息, 消息内容的字节数组);
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
//释放资源
channel.close();
connection.close();
}
}
[2] 创建消费者
下面分别创建两个消费者Consumer1和Consumer2。
消费者Consumer1:
/**
* 消费者1(工作模式)
*/
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、创建队列Queue,如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建.
// 这里可有可无,但是发送消息是必须得有该队列,否则消息会丢失
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 4、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
// handleDelivery(消费者标识, 消息包的内容, 属性信息(生产者的发送时指定), 读取到的消息)
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取消息:" + new String(body));
// 模拟消息处理延时,加个线程睡眠时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// basicConsume(队列名称, 是否自动确认, 回调对象)
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
消费者Consumer2:和消费者1几乎一模一样。
/**
* 消费者2(工作模式)
*/
public class Consumer2 {
// 队列名称
private static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、创建队列Queue,如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建.
// 这里可有可无,但是发送消息是必须得有该队列,否则消息会丢失
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 4、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
// handleDelivery(消费者标识, 消息包的内容, 属性信息(生产者的发送时指定), 读取到的消息)
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取消息:" + new String(body));
// 模拟消息处理延时,加个线程睡眠时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// basicConsume(队列名称, 是否自动确认, 回调对象)
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
[3] 运行结果
首先分别启动两个消费者(注意这里一定要先启动消费者)。
然后启动生产者,分别查看消费者控制台的打印信息,如下所示。
从结果来看,两个消费者对应的控制台是否竞争性的接收到消息。
轮询分发(round-robin)
上面的代码实现就是轮询分发的方式。现象:消费者1 处理完消息之后,消费者2 才能处理,它两这样轮着来处理消息,直到消息处理完成,这种方式叫轮询分发(round-robin)
,结果就是不管两个消费者谁忙,「数据总是你一个我一个」,不管消费者处理数据的性能,此时 autoAck = true。
注意:autoAck属性设置为true,表示消息自动确认。消费者在消费时消息的确认模式可以分为『自动确认和手动确认』。
- 自动确认:在队列中的消息被消费者读取之后会自动从队列中删除。不管消息是否被消费者消费成功,消息都会删除。
- 手动确认:当消费者读取消息后,消费端需要手动发送ACK用于确认消息已经消费成功了(也就是需要自己编写代码发送ACK确认),如果设为手动确认而没有发送ACK确认,那么消息就会一直存在队列中(前提是进行了持久化操作),后续就可能会造成消息重复消费,如果过多的消息堆积在队列中,还可能造成内存溢出,『手动确认消费者在处理完消息之后要及时发送ACK确认给队列』。
使用轮询分发的方式会有一个明显的缺点,例如消费者1 处理数据的效率很慢,消费者2 处理数据的效率很高,正常情况下消费者2处理的数据应该多一点才对,而轮询分发则不管你的性能如何,反正就是每次处理一个消息,对于这种情况可以使用公平分发的方式来解决。
公平分发(fair dipatch)
要实现公平分发
,操作分为两个步骤:
【1】、保证消息一次只分发一次,加一段关键性代码:
【2】、关闭自动确认,并且手动发送ACK给队列:
完整代码如下所示(分别修改两个消费者):
/**
* 消费者1(工作模式)
*/
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、创建队列Queue,如果没有一个名字叫work_queue的队列,则会创建该队列,如果有则不会创建.
// 这里可有可无,但是发送消息是必须得有该队列,否则消息会丢失
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
channel.basicQos(1);
// 4、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
// handleDelivery(消费者标识, 消息包的内容, 属性信息(生产者的发送时指定), 读取到的消息)
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者获取消息:" + new String(body));
// 模拟消息处理延时,加个线程睡眠时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动回执消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// basicConsume(队列名称, 是否自动确认, 回调对象)
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
修改完成之后再次运行,由于消费者1 设置处理完一个消息后睡眠2秒,而消费者2 为1 秒,所以预计输出的结果为: 消费者2 处理的消息大概是消费者1 的两倍左右,结果如下图所示。
3、发布订阅模式
发布订阅模式(Publish/Subscribe):这种模式需要涉及到交换机了,也可以称它为广播模式,消息通过交换机广播到所有与其绑定的队列中。
详细介绍:一个消费者将消息首先发送到交换机上(这里的交换机类型为fanout),然后交换机绑定到多个队列,这样每个发到fanout类型交换器的消息会被分发到所有的队列中,最后被监听该队列的消费者所接收并消费。如下图所示:
代码实现
[1] 创建生产者
/**
* 生产者(发布订阅模式)
*/
public class Producer {
// 交换机名称
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws Exception {
// 1、创建连接
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道
Channel channel = connection.createChannel();
// 3、连续发送10条消息
for (int i = 1; i <= 10; i++) {
String msg = "Hello RabbitMQ!!!~~~" + i;
System.out.println("生产者发送的消息:" + msg);
//basicPublish(交换机名称[默认Default Exchage],路由key[简单模式可以传递队列名称],消息其它属性,发送的消息内容)
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
}
//关闭资源
channel.close();
connection.close();
}
}
[2] 创建消费者
由于从这里开始涉及到交换机了,使用这里介绍一下四种交换机的类型:
- direct(直连):消息中的路由键(RoutingKey)如果和 Bingding 中的 bindingKey 完全匹配,交换器就将消息发到对应的队列中。是基于完全匹配、单播的模式。
- fanout(广播):把所有发送到fanout交换器的消息路由到所有绑定该交换器的队列中,fanout 类型转发消息是最快的。
- topic(主题):通过模式匹配的方式对消息进行路由,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。匹配规则:
- ① RoutingKey 和 BindingKey 为一个 点号 '.' 分隔的字符串。 比如: stock.usd.nyse;可以放任意的key在routing_key中,当然最长不能超过255 bytes。
- ② BindingKey可使用 * 和 # 用于做模糊匹配:*匹配一个单词,#匹配0个或者多个单词;
headers:不依赖于路由键进行匹配,是根据发送消息内容中的headers属性进行匹配,除此之外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了。
消费者1:
注意:在发送消息前,RabbitMQ服务器中必须的有队列,否则消息可能会丢失,如果还涉及到交换机与队列绑定,那么就得先声明交换机、队列并且设置绑定的路由值(Routing Key),以免程序出现异常,由于本例所有的声明都是在消费者中,所以我们首先要启动消费者。如果RabbitMQ服务器中已经存在了声明的队列或者交换机,那么就不在创建,如果没有则创建相应名称的队列或者交换机。
/**
* 消费者1(发布订阅模式)
*/
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME1 = "fanout_queue1";
// 交换机名称
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
/* 3、声明交换机
* exchange 参数1:交换机名称
* type 参数2:交换机类型
* durable 参数3:交换机是否持久化
*/
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true);
// 4、声明队列Queue queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加参数)
channel.queueDeclare(QUEUE_NAME1, true, false, false, null);
// 5、绑定队列和交换机 queueBind(队列名, 交换机名, 路由key[交换机的类型为fanout ,routingKey设置为""])
channel.queueBind(QUEUE_NAME1, EXCHANGE_NAME, "");
// 6、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息信息
String message = new String(body, "utf-8");
System.out.println("交换机名称:" + exchange + ",消费者获取消息: " + message);
}
};
channel.basicConsume(QUEUE_NAME1, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
消费者2:和消费者1几乎一模一样
/**
* 消费者2(发布订阅模式)
*/
public class Consumer2 {
// 队列名称
private static final String QUEUE_NAME2 = "fanout_queue2";
// 交换机名称
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、声明交换机,如果没有名称为EXCHANGE_NAME的交换机则创建,有则不创建
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true);
// 4、声明队列Queue。channel.queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加参数)
channel.queueDeclare(QUEUE_NAME2, true, false, false, null);
// 5、绑定队列和交换机。channel.queueBind(队列名, 交换机名, 路由key[fanout交换机的routingKey设置为""])
channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "");
// 6、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息信息
String message = new String(body, "utf-8");
System.out.println("交换机名称:" + exchange + ",消费者获取消息: " + message);
}
};
channel.basicConsume(QUEUE_NAME2, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
[3] 运行结果
首先分别启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;到达『广播』的效果,如下所示。
在执行完测试代码后,可以到RabbitMQ的管理后台找到Exchanges
选项卡,点击说明的 fanout_exchange
交换机,可以查看到如下的绑定:
[4] 简单总结
发布订阅模式引入了交换机的概念,所以相对前面的类型更加灵活广泛一些。这种模式需要设置类型为fanout的交换机,并且将交换机和队列进行绑定,当消息发送到交换机后,交换机会将消息发送到所有绑定的队列,最后被监听该队列的消费者所接收并消费。发布订阅模式也可以叫广播模式,不需要RoutingKey的判断。
发布订阅模式与工作队列模式的区别:
1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。
4、路由模式(精确匹配)
路由模式(Routing)的特点:
- 该模式的交换机为direct,意思为定向发送,精准匹配。
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
RoutingKey
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息。
详细介绍:生产者将消息发送到direct交换器,同时生产者在发送消息的时候会指定一个路由key,而在绑定队列和交换器的时候又会指定一个路由key,那么消息只会发送到相应routing key相同的队列,然后由监听该队列的消费者进行消费消息。模型如下图所示:
代码实现
[1] 创建生产者
/**
* 生产者(路由模式)
*/
public class Producer {
// 交换机名称
private static final String EXCHANGE_NAME = "routing_exchange";
public static void main(String[] args) throws Exception {
// 1、创建连接
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、发送消息,连续发3条
for (int i = 0; i < 3; i++) {
String routingKey = "";
//发送消息的时候根据相关逻辑指定相应的routing key。
switch (i) {
case 0: //假设i=0,为error消息
routingKey = "error";
break;
case 1: //假设i=1,为info消息
routingKey = "info";
break;
case 2: //假设i=2,为warning消息
routingKey = "warning";
break;
}
// 要发送的消息
String message = "Hello Message!!!~~~" + routingKey;
// 消息发送 channel.basicPublish(交换机名称,路由key,消息其它属性,消息内容)
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("utf-8"));
System.out.println("生产者发送的消息:" + message);
}
//释放资源
channel.close();
connection.close();
}
}
[2] 创建消费者
消费者1:
/**
* 消费者1(路由模式)
*/
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME1 = "routing_queue1";
// 交换机名称
private static final String EXCHANGE_NAME = "routing_exchange";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、声明交换机(有则不创建,无则创建) channel.exchangeDeclare(交换机名字,交换机类型,是否持久化)
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
// 4、声明队列Queue。channel.queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加参数)
channel.queueDeclare(QUEUE_NAME1, true, false, false, null);
// 5、根据指定的routingKey绑定队列和交换机 channel.queueBind(队列名, 交换机名, 路由key)
channel.queueBind(QUEUE_NAME1, EXCHANGE_NAME, "error");
// 6、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取路由的key
String routingKey = envelope.getRoutingKey();
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息信息
String message = new String(body, "utf-8");
System.out.println("路由Key:" + routingKey + ", 交换机名称:" + exchange + ", 消费者获取消息: " + message);
}
};
channel.basicConsume(QUEUE_NAME1, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
消费者2:
/**
* 消费者2(路由模式)
*/
public class Consumer2 {
// 队列名称
private static final String QUEUE_NAME2 = "routing_queue2";
// 交换机名称
private static final String EXCHANGE_NAME = "routing_exchange";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、声明交换机(有则不创建,无则创建) channel.exchangeDeclare(交换机名字,交换机类型,是否持久化)
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true);
// 4、声明队列Queue。channel.queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加参数)
channel.queueDeclare(QUEUE_NAME2, true, false, false, null);
// 5、根据指定的routingKey绑定队列和交换机 channel.queueBind(队列名, 交换机名, 路由key)
channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "error");
channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "info");
channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "warning");
// 6、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取路由的key
String routingKey = envelope.getRoutingKey();
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息信息
String message = new String(body, "utf-8");
System.out.println("路由Key:" + routingKey + ", 交换机名称:" + exchange + ", 消费者获取消息: " + message);
}
};
channel.basicConsume(QUEUE_NAME2, true, defaultConsumer);
}
}
[3] 运行结果
首先分别启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达『按照需要接收』的效果。
消费者1绑定的交换机和队列的路由Key为error,所以只要生产者发送消息时带有error的routingKey它都能够获取到消息。
消费者2绑定的交换机和队列的路由Key为error、info、warning,所以只要生产者发送消息时带有这3种的routingKey它都能够获取到消息。
[4] 简单总结
- Routing模式需要将交换机设置为Direct类型。
- Routing模式要求队列在绑定交换机时要指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列。
5、Topic模式(模糊匹配)
Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。但是Topic类型的Exchange可以让队列在绑定Routing key 的时候使用通配符进行匹配,也就是模糊匹配,这样与之前的模式比起来,它更加的灵活!
Topic主题模式的Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: log.insert ,它的通配符规则如下:
- *:匹配不多不少恰好1个词
- #:匹配0或多个单词
简单举例:
log.*:只能匹配log.error,log.info 等
log.#:能够匹配log.insert,log.insert.abc,log.news.update.abc 等
图解:
- 红色Queue:绑定的是
usa.#
,因此凡是以usa.
开头的routing key
都会被匹配到 - 黄色Queue:绑定的是
#.news
,因此凡是以.news
结尾的routing key
都会被匹配
代码实现
[1] 创建生产者
/**
* 生产者(Topic主题模式)
*/
public class Producer {
// 交换机名称
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws Exception {
// 1、创建连接
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、发送消息
for (int i = 0; i < 4; i++) {
String routingKey = "";
//发送消息的时候根据相关逻辑指定相应的routing key。
switch (i) {
case 0: //假设i=0,为select消息
routingKey = "log.select";
break;
case 1: //假设i=1,为info消息
routingKey = "log.delete";
break;
case 2: //假设i=2,为log.news.add消息
routingKey = "log.news.add";
break;
case 3: //假设i=3,为log.news.update消息
routingKey = "log.news.update";
break;
}
// 要发送的消息
String message = "Hello Message!!!~~~" + routingKey;
// 消息发送 channel.basicPublish(交换机名称,路由key,消息其它属性,消息内容)
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("utf-8"));
System.out.println("生产者发送的消息:" + message);
}
// 关闭资源
channel.close();
connection.close();
}
}
[2] 创建消费者
消费者1:接收所有与log.*
相匹配的路由key队列中的消息
/**
* 消费者(Topic模式)
*/
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME1 = "topic_queue1";
// 交换机名称
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、声明交换机(有则不创建,无则创建) channel.exchangeDeclare(交换机名字,交换机类型,是否持久化)
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
// 4、声明队列Queue channel.queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加参数)
channel.queueDeclare(QUEUE_NAME1, true, false, false, null);
// 5、根据指定的routingKey绑定队列和交换机,设置路由key channel.queueBind(队列名, 交换机名, 路由key)
channel.queueBind(QUEUE_NAME1, EXCHANGE_NAME, "log.*");
// 6、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取路由的key
String routingKey = envelope.getRoutingKey();
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息信息
String message = new String(body, "utf-8");
System.out.println("路由Key:" + routingKey + ", 交换机名称:" + exchange + ", 消费者获取消息: " + message);
}
};
channel.basicConsume(QUEUE_NAME1, true, defaultConsumer);
//注意,消费者这里不建议关闭资源,让程序一直处于读取消息的状态
}
}
消费者2:接收所有与log.#
相匹配的路由key队列中的消息
/**
* 消费者(Topic模式)
*/
public class Consumer2 {
// 队列名称
private static final String QUEUE_NAME2 = "topic_queue2";
// 交换机名称
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws Exception {
// 1、获取连接对象
Connection connection = ConnectionUtils.getConnection();
// 2、创建通道(频道)
Channel channel = connection.createChannel();
// 3、声明交换机(有则不创建,无则创建) channel.exchangeDeclare(交换机名字,交换机类型,是否持久化)
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC, true);
// 4、声明队列Queue。channel.queueDeclare(队列名称,是否持久化,是否独占本连接,是否自动删除,附加参数)
channel.queueDeclare(QUEUE_NAME2, true, false, false, null);
// 5、根据指定的routingKey绑定队列和交换机 channel.queueBind(队列名, 交换机名, 路由key)
channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, "log.#");
// 6、监听队列,接收消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//获取路由的key
String routingKey = envelope.getRoutingKey();
//获取交换机信息
String exchange = envelope.getExchange();
//获取消息信息
String message = new String(body, "utf-8");
System.out.println("路由Key:" + routingKey + ", 交换机名称:" + exchange + ", 消费者获取消息: " + message);
}
};
channel.basicConsume(QUEUE_NAME2, true, defaultConsumer);
}
}
[3] 运行结果
首先分别启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达『按照需要接收』的效果。
消费者1的路由key匹配规则为log.*
,所有该路由规则的绑定的队列应该只有2条信息,结果如下所示:
消费者2的路由key匹配规则为log.#
,它能够匹配以log.
开头的所有路由key,所有该路由规则的绑定的队列应该只有4条信息,结果如下所示:
最后查看一下交换机与队列绑定的相关信息。
[4] 简单总结
Topic主题模式
需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列。Topic主题模式
可以实现Publish/Subscribe发布与订阅模式
和Routing路由模式
的功能;只是Topic在配置routing key 的时候可以使用通配符,所以显得更加灵活。