目录
三、工作模式队列-消息公平分发(fair dispatch)
四、Publish/Subscribe-消息的发布与订阅模式队列
问题:RabbitMQ本身是基于异步的消息处理,是否可以同步实现?
六种模式队列
-
简单队列模式:最简单的工作队列,其中一个消息生产者,一个消息消费者,一个队列。也称为点对点模式
-
工作模式:一个消息生产者,一个交换器,一个消息队列,多个消费者。同样也称为点对点模式
-
发布/订阅模式:无选择接收消息,一个消息生产者,一个交换器,多个消息队列,多个消费者。称为发布/订阅模式
-
路由模式:在发布/订阅模式的基础上,有选择的接收消息,也就是通过 routing 路由进行匹配条件是否满足接收消息。
-
主题模式:同样是在发布/订阅模式的基础上,根据主题匹配进行筛选是否接收消息,比第四类更灵活。
-
RPC模式:与上面其他5种所不同之处,类模式是拥有请求/回复的。也就是有响应的,上面5种都没有。
RabbitMQ 术语
1. Producing
Producing意思不仅仅是发送消息。发送消息的程序叫做producer生产者。
2. Queue
Queue是一个消息盒子的名称。它存活在 RabbitMQ 里。虽然消息流经 RabbitMQ 和你的应用程序,但是他们只能在 Queue 里才能被保存。Queue 没有任何边界的限制,你想存多少消息都可以,它本质上是一个无限的缓存。许多生产者都可以向一个 Queue 里发送消息,许多消费者都可以从一个 Queue 里接收消息。
3. Consuming
Consuming 的意思和接收类似。等待接收消息的程序叫做消费者。
注意:生产者,消费者和代理不一定非要在同一台机器上。
一、简单模式队列
1. Sending
/*
简单队列模式(发送者)
*/
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
try( // 创建连接
Connection connection = factory.newConnection();
// 构建信道
Channel channel = connection.createChannel();
){
/**
* 声明队列
* 第一个参数queue:队列名称
* 第二个参数durable:是否持久化
* 第三个参数Exclusive:排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。
* 这里需要注意三点:
* 1. 排他队列是基于连接可见的,同一连接的不同通道是可以同时访问同一个连接创建的排他队列的。
* 2. "首次",如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同。
* 3. 即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的。
* 这种队列适用于只限于一个客户端发送读取消息的应用场景。
* 第四个参数Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。
* 这种队列适用于临时队列。
*/
// 声明队列(队列名称,是否持久化,排他队列,自动删除)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 发送数据信息
String message = "长津平战役";
// 发送操作
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消息发送成功以后,通过RabbitMQ管理界面可以看到队列的相关信息
2. Receiving
消息的发送者只是发送一个消息,我们的接收者需要不断的监听消息,并把它们打印出来。
/*
简单队列模式(接收)
*/
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
// 创建连接
Connection connection = factory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
// 监听队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// ---------------------之前旧版本的写法-------begin-----------
/*
// 获取消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 获取消息并在控制台打印
String message = new String(body, "utf-8");
System.out.println(" [x] Received '" + message + "'");
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);
*/
// ---------------------之前旧版本的写法--------end------------
// 获取消息
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 ->{
});
}
}
消息接收成功以后,通过RabbitMQ管理界面可以看到队列的相关信息
3. 测试
(1)运行Send.java
(2)运行Recv.java
4. 总结
问题:
如果任务量很大,消息得不到及时的消费会造成队列积压,问题非常严重,比如内存溢出,消息丢失等。
解决:配置多个消费者消息。
总结:
简单队列-处理消息效率不高,吞吐量较低,不适合生成环境
二、Work queues-工作模式队列
前言
当消息消费者只有一个,当消息量非常大时,单个消费者处理消息就会变得很慢,同时给节点页带来很大压力,导致消息堆积越来越多。对于这种情况,RabbitMQ 提供了工作队列模式,通过工作队列提供做个消费者,对MQ产生的消息进行消费,提高MQ消息的吞吐率,降低消息的处理时间。
1. Sending
/**
* 工作模式-轮询(发送)
*/
public class Send {
private final static String QUEUE_NAME = "work";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
try ( // 创建连接
Connection connection = factory.newConnection();
// 构建信道
Channel channel = connection.createChannel();
) {
// 声明队列(队列名称,是否持久化,排他队列,自动删除)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 20; i++) {
// 准备数据信息
String message = "work" + i;
// 发送操作
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
2. Receiving
(1) Recv01.java
/**
* 工作模式-轮询(接收)
*/
public class Recv01 {
private final static String QUEUE_NAME = "work";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
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 -> {
});
}
}
(2) Recv02.java
/**
* 工作模式-轮询(接收)
*/
public class Recv02 {
private final static String QUEUE_NAME = "work";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
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 -> {
});
}
}
3. 测试
(1)运行Send
(2)运行Recv
4. 总结
从结果可以看出消息被平均分配到两个消费方,来对消息进行处理,提高了消息处理效率,创建多个消费者来对消息进行处理。这里RabbitMQ采用轮询来对消息进行分发时保证了消息被平均分配到每个消费方。
但是引入新的问题:
真正的生产环境下,对于消息的处理基本不会像我们现在看到的这样,每个消费方处理的消息数量是平均分配的,比如因为网络原因,机器cpu,内存等硬件问题,消费方处理消息时同类消息不同机器进行处理时消耗时间也是不一样的。
例如:1号消费者消费1条消息时1秒,2号消费者消费1条消息是5秒,对于1号消费者比2号消费者处理消息快,那么在分配消息时就应该让1号消费者多收到消息进行处理,也即是我们通常所说的”能者多劳”,同样Rabbitmq对于这种消息分配模式提供了支持。
解决:采用消息公平分发
总结:
工作队列-消息轮询分发-消费者收到的信息数量平均分配,单位时间内消息处理速度加快,提高了吞吐量。
三、工作模式队列-消息公平分发(fair dispatch)
在上个案例中对消息分发采用的是默认轮询分发,消息应答采用的自动应答模式,这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将第n条消息发送给第n个消费者。
为了解决这个问题,我们使用 basicQos(prefetchCount = 1) 方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。当消息处理完毕后,有了反馈,才会进行第二次发送。
1. Sending
/*
工作模式-公平分发(发送)
*/
public class Send {
private final static String QUEUE_NAME = "fair";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
try( // 创建连接
Connection connection = factory.newConnection();
// 构建信道
Channel channel = connection.createChannel();
){
// 声明队列(队列名称,是否持久化,排他队列,自动删除)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
for(int i =0;i<20;i++){
// 准备数据信息
String message = "work"+i;
// 发送操作
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
2. Receiving
(1) Recv01.java
/*
工作模式-公平分发 (接收)
*/
public class Recv01 {
private final static String QUEUE_NAME = "fair";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
// 创建连接
Connection connection = factory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
/*
限制RabbitMQ只发不超过1条的信息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 监听队列(队列名称,是否持久化,排他队列,自动删除)
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 + "'");
// 模拟程序执行所耗时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {
});
}
}
(2) Recv02.java
/*
工作模式-公平分发 (接收)
*/
public class Recv02 {
private final static String QUEUE_NAME = "fair";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
// 创建连接
Connection connection = factory.newConnection();
// 创建信道
Channel channel = connection.createChannel();
/*
限制RabbitMQ只发不超过1条的信息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 监听队列(队列名称,是否持久化,排他队列,自动删除)
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 + "'");
// 模拟程序执行所耗时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
// 监听队列
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {
});
}
}
3. 测试
(1)运行Send
(2)运行Recv
4. 总结
从结果可以看出1号消费者消费消息数量明显高于2号,即消息通过fair 机制被公平分发到每个消费者。
问题:
生产者产生的消息只可以被一个消费者消费,可不可以被多个消费者消费呢?
解决:采用发布于订阅模式。
总结:
工作队列-公平轮询发布-根据不同消费者机器硬件配置,消息处理速度不同,收到的信息数量也不同,通常速度快的处理的消息数量比较多,最大化使用计算机资源。使用于生产环境。
四、Publish/Subscribe-消息的发布与订阅模式队列
对于微信公众号,相信每个人都订阅过,当公众号发送新的消息后,对于订阅过该公众号的所有用户均可以收到消息,这个场景大家都能明白,同样对于RabbitMQ消息的处理也支持这种消息处理,当生产者把消息投送出去后,不同的消费者均可以对该消息进行消费,而不是消息被一个消费者消费后就立即从队列中删除,对于这种消息处理,我们通常称之为消息的发布与订阅模式,凡是消费者订阅了该消息,均能够收到对应消息进行处理,比较常见的如用户注册操作。
从图中看到:
-
消息产生后不是直接投送到队列中,而是将消息先投送给Exchange交换机,然后消息经过Exchange 交换机投递到相关队列
- 多个消费者消费的不再是同一个队列,而是每个消费者消费属于自己的队列
1. Sending
/**
* 发布与订阅模式队列-fanout广播模式-消息发送者
*/
public class Send {
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
try (Connection connection = factory.newConnection();
//构建信道
Channel channel = connection.createChannel()) {
//绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//准备数据信息
String message = "work";
//发送操作
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
2. Receiving
(1) Recv01
/**
* 工作模式-轮询(接收)
*/
public class Recv01 {
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); // fanout交换机
//获取队列名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到交换机
channel.queueBind(queueName,EXCHANGE_NAME,"");
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(queueName, true, deliverCallback, consumerTag -> {
});
}
}
(2)Recv02
/**
* 工作模式-轮询(接收)
*/
public class Recv02 {
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
// 绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); // fanout交换机
//获取队列名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到交换机
channel.queueBind(queueName,EXCHANGE_NAME,"");
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(queueName, true, deliverCallback, consumerTag -> {
});
}
}
3. 测试
(1)运行Send
(2)运行Recv
4. 总结
从结果可以看出生产者发送了一条消息,Recv01和Recv02发送的消费者均可以收到消息进行后续处理。
问题:
生产者产生的消息所有消费者都可以消费,可不可以指定某些消费者消费呢?
解决:采用direct路由模式。
五、Routing-路由模式队列
根据上一个案例可以看到,生产者将消息投送给交换机后,消息经交换机分发到不同的队列即:交换机收到消息,默认对于绑定到每个交换机的队列均会接收到交换机分发的消息,对于案例03的交换机的消息分发Exchange Types为 fanout 类型,通常在真正项目开发时会遇到这种情况:在对项目信息输出日志进行收集时,会把日志(errorwarning,info)分类进行输出,这时通过Exchange Types中的 direct 类型就可以实现,针对不同的消息,在对消息进行消费时,通过 Exchange types 以及 Routing key 设置的规则 ,便可以将不同消息路由到不同的队列中然后交给不同消费者进行消费操作。
从图中可以看出:
- 生产者产生的消息投给交换机。
-
交换机投送消息时的Exchange Types为direct类型
-
消息通过定义的Routing Key被路由到指定的队列进行后续消费
1. Sending
/**
* direct路由模式队列-消息发送者
*/
public class Send {
private final static String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] argv) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
try (
// 通过工厂创建连接
Connection connection = factory.newConnection();
//构建信道
Channel channel = connection.createChannel()) {
//绑定交换机 direct : 路由模式
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//准备数据信息
String message = "work-direct";
//发送操作
channel.basicPublish(EXCHANGE_NAME, "warn", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
2. Receiving
(1)Recv01
/**
* direct路由模式队列-消息发送者
*/
public class Recv01 {
private final static String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
// 绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//获取队列名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到交换机
channel.queueBind(queueName,EXCHANGE_NAME,"error");
channel.queueBind(queueName,EXCHANGE_NAME,"info");
channel.queueBind(queueName,EXCHANGE_NAME,"warn");
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(queueName, true, deliverCallback, consumerTag -> {
});
}
}
(2)Recv02
/**
* direct路由模式队列-消息发送者
*/
public class Recv02 {
private final static String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
// 绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//获取队列名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到交换机
channel.queueBind(queueName,EXCHANGE_NAME,"error");
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(queueName, true, deliverCallback, consumerTag -> {
});
}
}
3. 测试
4. 总结
从结果可以看出生产者发送了多条设置了路由规则的消息,消费者可以根据具体的路由规则消费对应队列中的消息,而不是所有消费者都可以消费所有消息了。
问题:
解决:采用topic主题模式
六、Topics-主题模式队列
通过案例04看到消息通过交换机Exchange Type以及Routing Key规则,可以将消息路由到指定的队列,也符合在工作中的场景去使用的一种方式,对于RabbitMq 除了direct 模式外,Mq 同样还提供了 topics 主题模式来对消息进行匹配路由,比如在项目开发中,拿商品模块来说,对于商品的查询功能在对商品进行查询时我们将查询消息路由到查询对应队列,而对于商品的添加、更新、删除等操作我们统一路由到另外一个队列来进行处理,此时采用direct 模式可以实现,但对于维护的队列可能就不太容易进行维护,如果涉及模块很多,此时对应队列数量就很多,此时我们就可以通过 topic 主题模式来对消息路由时进行匹配,通过指定的匹配模式将消息路由到匹配到的队列中进行后续处理。
-
routing key 为一个句点号 . 分隔的字符串(我们将被句点号 . 分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse” 、 “nyse.vmw” 、“quick.orange.rabbit”
-
routing key 中可以存在两种特殊字符 * 与 # ,用于做模糊匹配,其中 * 用于匹配一个单词, # 用于匹配多个单词(可以是零个)
以上图中的配置为例:
-
routingKey=”quick.orange.rabbit” 的消息会同时路由到 Q1 与 Q2 ,
-
routingKey=”lazy.orange.fox” 的消息会路由到 Q1 , Q2,
-
routingKey=”lazy.brown.fox” 的消息会路由到 Q2 ,
-
routingKey=”lazy.pink.rabbit” 的消息会路由到 Q2 ;
-
routingKey=”quick.brown.fox”;
-
routingKey=”orange”;
-
routingKey=”quick.orange.male.rabbit” 的消息将会被丢弃,因为它们没有匹配任何bindingKey 。
1. Sending
/**
* direct路由模式队列-消息发送者
*/
public class Send {
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
try (Connection connection = factory.newConnection();
//构建信道
Channel channel = connection.createChannel()) {
//绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
//准备数据信息
String message = "work-topic";
//发送操作
channel.basicPublish(EXCHANGE_NAME, "lazy.abc.dasdsa.dasdsa.das.das.eqw", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
2. Receiving
(1)Recv01
/**
* direct路由模式队列-消息发送者
*/
public class Recv01 {
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
// 绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//获取队列名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到交换机
channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*");
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(queueName, true, deliverCallback, consumerTag -> {
});
}
}
(2)Recv02
/**
* direct路由模式队列-消息发送者
*/
public class Recv02 {
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
//创建连接
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
// 绑定交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//获取队列名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到交换机
channel.queueBind(queueName,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(queueName,EXCHANGE_NAME,"lazy.#");
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(queueName, true, deliverCallback, consumerTag -> {
});
}
}
3. 测试
4. 总结
问题:RabbitMQ本身是基于异步的消息处理,是否可以同步实现?
解决:采用RPC模式
七、RPC-远程过程调用模式队列
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程 过程调用)。在RabbitMQ中也支持RPC。
RabbitMQ中实现RPC的机制是:
-
客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14种properties,这些属性会随着消息一起发送)中设置两个值replyTo (一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和 correlationId (此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)
-
服务器端收到消息并处理
-
服务器端处理完消息后,将生成一条应答消息到 replyTo 指定的 Queue ,同时携带 correlationId 属性
1. Server
/**
* RPC模式队列-服务端
*/
public class RPCServer {
// 队列名称
private static final String RPC_QUEUE_NAME = "rpc_queue";
/**
* 计算斐波那契数列
*
* @param n
* @return
*/
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] args) {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
try {
// 通过工厂创建连接
final Connection connection = factory.newConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.queuePurge(RPC_QUEUE_NAME);
/*
限制RabbitMQ只发不超过1条的消息给同一个消费者。
当消息处理完毕后,有了反馈,才会进行第二次发送。
*/
int prefetchCount = 1;
channel.basicQos(prefetchCount);
System.out.println(" [x] Awaiting RPC requests");
Object monitor = new Object();
// 获取消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
// 获取replyTo队列和correlationId请求标识
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
String response = "";
try {
// 接收客户端消息
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
// 服务端根据业务需求处理
response += fib(n);
} catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
// 将处理结果发送至replyTo队列同时携带correlationId属性
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps,
response.getBytes("UTF-8"));
// 手动回执消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// RabbitMq consumer worker thread notifies the RPC server owner thread
// RabbitMq消费者工作线程通知RPC服务器其他所有线程运行
synchronized (monitor) {
monitor.notify();
}
}
};
// 监听队列
/*
autoAck = true代表自动确认消息
autoAck = false代表手动确认消息
*/
boolean autoAck = false;
channel.basicConsume(RPC_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> {
});
// Wait and be prepared to consume the message from RPC client.
// 线程等待并准备接收来自RPC客户端的消息
while (true) {
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
2. Client
/**
* RPC模式队列-客户端
*/
public class RPCClient implements AutoCloseable {
private Connection connection;
private Channel channel;
// 队列名称
private String requestQueueName = "rpc_queue";
// 初始化连接
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.182.101");
factory.setPort(5672);
factory.setUsername("crm");
factory.setPassword("crm");
factory.setVirtualHost("crm");
connection = factory.newConnection();
channel = connection.createChannel();
}
public static void main(String[] args) {
try (RPCClient fibonacciRpc = new RPCClient()) {
for (int i = 0; i < 10; i++) {
String i_str = Integer.toString(i);
System.out.println(" [x] Requesting fib(" + i_str + ")");
// 请求服务端
String response = fibonacciRpc.call(i_str);
System.out.println(" [.] Got '" + response + "'");
}
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}
// 请求服务端
public String call(String message) throws IOException, InterruptedException {
// correlationId请求标识ID
final String corrId = UUID.randomUUID().toString();
// 获取队列名称
String replyQueueName = channel.queueDeclare().getQueue();
// 设置replyTo队列和correlationId请求标识
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
// 发送消息至队列
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
// 设置线程等待,每次只接收一个响应结果
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
// 接受服务器返回结果
String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
// 将给定的元素在给定的时间内设置到线程队列中,如果设置成功返回true, 否则返回false
response.offer(new String(delivery.getBody(), "UTF-8"));
}
}, consumerTag -> {
});
// 从线程队列中获取值,如果线程队列中没有值,线程会一直阻塞,直到线程队列中有值,并且取得该值
String result = response.take();
// 从消息队列中丢弃该值
channel.basicCancel(ctag);
return result;
}
// 关闭连接
public void close() throws IOException {
connection.close();
}
}
3. 测试
(1)运行Server
(2)运行Client
(3)查看Server
兄弟,你触到我的底线了,快块三连(狗头保命)。。。。