有一个支付场景,大家都使用过微信、支付宝支付,比如自动售卖机购买饮料,需要扫码、支付、查询支付结果,出商品。
我们可以发现,售卖机请求支付二维码后,这个请求就结束了,查询支付结果又是另外一个请求,分两种方式,一种是使用http的方式,另外一种是使用推送(消息队列)的方式。不管使用什么方式,我们可以发现,支付的整套流程并不是一次请求就可以搞定。如果一次搞定,高并发的时候服务器的压力就很大,一直在等待支付结果,但用户什么时候支付也不知道。再比如鱼皮哥的做的智能BI项目,AI返回结果的时间会根据用户输入的信息来决定。
消息队列就可以很好解决了同步的问题,采用异步的实现方式。那么消息队列是如何解决同步的问题?
存储消息的队列。
消息队列的应用场景:
消息队列的好处
消息队列的缺点:
中间件
Rabbit MQ是消息队列的一种,生态好,好学习,易于理解,时效性强,支持很多不同语言的客户端,扩展性、可用性都很不错。学习性价比非常高的消息队列,适用于绝大多数中小规模分布式系统。
MQ是消息通信的模型,并发具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
那么接下来我们还需要了解什么才能快速入门?
生产者(Publisher)、交换机(Exchange)、路由(Routes)、队列(Queue)、消费者(Consumer)
基本流程
每一个环节它都是如何实现,怎么进行?这个只是基本的流程,还有更加细节的。但这里我们可以快速入门体验一波。
rabbitmq-plugins.bat enable rabbitmq_management
案例:我们怎么实现,生产一条信息,发送给消费者?
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.17.0version>
dependency>
(1)首先,需要和rabbit mq建立连接
(2)然后使用Channel,创建出可以进行操作队列、发送消息的工具。
(3)声明队列
(4)发送消息
public class HelloWorldProducer {
//队列名
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
//在 RabbitMQ 中,Channel 指的是在连接(Connection)上创建的一个逻辑通道,用来进行发送和接收消息的操作。每一个 Channel 都会拥有独立的 ID,可以根据这个 ID 与 RabbitMQ 服务器进行通信。
//通过 Channel,应用程序可以进行以下操作:
// 声明队列(Queue)和交换器(Exchange)。
// 将队列绑定到交换器上。
// 发布消息到指定的交换器上。
// 消费指定队列上的消息。
//在 RabbitMQ 中,每个 Connection 都支持多个 Channel,应用程序可以根据自己的需求创建多个 Channel,从而实现并发和优化网络带宽的利用。但是需要注意,对于一个 Connection 可能存在的并发限制,在应用程序中需要合理控制 Channel 的数量。
Channel channel = connection.createChannel()) {
//这段代码是通过 RabbitMQ 的 Java 客户端创建一个名为 `QUEUE_NAME` 的队列。其中,代码参数的含义如下:
// `QUEUE_NAME`:队列名,即要创建的队列的名称。
// `false`:指定是否为持久化队列。设置为 `false` 表示创建的队列在 RabbitMQ 服务器重启后会被删除。
// `false`:指定是否为排他队列。设置为 `false` 表示队列可以被其他连接访问。
// `false`:指定队列是否应该自动删除。设置为 `false` 表示当没有任何消费者使用该队列时,该队列不会自动删除。
// `null`:指定队列的属性。设置为 `null` 表示不需要为队列设置任何属性。
//当该方法被成功执行后,就可以使用 `channel.basicPublish()` 方法向队列发送消息,并使用 `channel.basicConsume()` 方法从队列中获取消息。该队列的状态信息也可以通过 `com.rabbitmq.client.AMQP.Queue.DeclareOk` 对象来进行监控。
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message = "Hello World";
//这段代码是通过 RabbitMQ 的 Java 客户端向名为 `QUEUE_NAME` 的队列发送消息。具体来说,代码参数的含义如下:
// `""`:交换机名,这里使用空字符串表示直接发送到队列中而不经过交换机。
// `QUEUE_NAME`:队列名,即要发送消息的队列的名称。
// `null`:消息的其他属性,这里未设置任何属性。
// `message.getBytes(StandardCharsets.UTF_8)`:消息体的字节数组,即要发送的消息内容。
//当该方法被成功执行后,消息就会被发送到 `QUEUE_NAME` 队列中等待消费者来处理。如果需要将消息发送到指定的交换机(Exchange),则需要将第一个参数 `""` 修改为实际的交换机名称,并设置好 Exchange 的类型、路由键等信息。
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
(1)首先,需要和rabbit mq建立连接
(2)然后使用Channel,创建出可以进行操作队列、发送消息的工具。
(3)声明队列
(4)监听消息
public class HelloWorldConsumer {
//队列名
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//用于处理消费者接收到消息的回调函数
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
//`consumerTag`:是消费者在订阅队列时分配到的唯一标识符,用于在取消订阅时标识消费者。
public void handle(String consumerTag, Delivery message) throws IOException {
String s = new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println(consumerTag + " [x] Received '" + s + "'");
}
};
//
//`QUEUE_NAME`:表示要消费的队列名。
//`true`:表示自动确认消息,当消费者接收到一条消息后就会自动向消息队列发送确认消息,告诉消息队列这条消息已经被消费处理完成。
//`deliverCallback`:表示接收到消息后的处理逻辑,将在 `handle` 方法中执行。
//`consumerTag -> {}`:表示用于接收消费者标识的回调函数。在上面的 `basicCancel` 方法中同样需要传入该回调函数中的消费者标识才能成功取消消费者的订阅。
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
System.out.println(consumerTag);
});
}
}
好啦,hello world就结束了。注意:生产者和消费者,创建的队列,必须一摸一样,设置也是。
分为work消息模型和publish/subscribe模型。
谁抢到,谁执行,work消息模型,竞争消费者模式。
其实和单向发送的写法一样,只不过多了一个消费者
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.apache.poi.ss.formula.functions.T;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* 单向发送,多个消费者
*/
public class MultiProducer {
private final static String TASK_QUEUE_NAME = "multi_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try(Connection connection = factory.newConnection();
Channel channel = connection.createChannel()){
channel.queueDeclare(TASK_QUEUE_NAME,true,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String s = scanner.nextLine();
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
s.getBytes("UTF-8"));
System.out.println(" [x] send :"+ s);
}
}
}
}
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 单向发送,多个消费者
*/
public class MultiConsumer {
private final static String TASK_QUEUE_NAME = "multi_queue";
public static void main(String[] args) throws Exception {
//创建到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
for (int i = 0; i < 2; i++) {
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
//`1`:表示预取的消息数量为 `1`。这个值指定消费者从消息队列一次预取的消息数量,也称为“批量大小”或“流量控制窗口”。在 `basicConsume` 方法中,默认值为 `0`,即不限制预取消息的数量,这会导致消费者处理消息的效率过低,同时也会对 RabbitMQ 服务器的资源造成压力,因此需要手动设置合理的预取消息数量,根据实际场景和消费者的处理能力来调整该值。
// 建议将 `Qos` 设置为 1,单次拉取消息一个,这种场景分配是因为可以保证每一次处理完一个消息之后,再去请求下一条消息,降低服务器的压力,从而防止消息积压。
channel.basicQos(1);
//用于处理消费者接收到消息的回调函数
int finalI = i;
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
//`consumerTag`:是消费者在订阅队列时分配到的唯一标识符,用于在取消订阅时标识消费者。
public void handle(String consumerTag, Delivery message) throws IOException {
try {
// 处理工作
System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");
//用于手动确认消息已经被消费的方法
//`message.getEnvelope().getDeliveryTag()`:获取消息信封中的投递标签(delivery tag),标识消息的唯一编号,用于在手动确认消息时指定要确认的消息。
//`false`:表示不批量确认,该参数为 `true` 时表示要一次性确认多个投递标签指定的消息。在这里,为了保证消息确认的可靠性和准确性,只确认单个消息,因此该参数值为 `false`。
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
// 停 20 秒,模拟机器处理能力有限
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
//参数三,表示是否重新入队,可用于重试。这里不进行
channel.basicNack(message.getEnvelope().getDeliveryTag(), false, false);
} finally {
System.out.println(" [x] Done");
// channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
}
};
//
//`QUEUE_NAME`:表示要消费的队列名。
//`true`:表示自动确认消息,当消费者接收到一条消息后就会自动向消息队列发送确认消息,告诉消息队列这条消息已经被消费处理完成。
//`deliverCallback`:表示接收到消息后的处理逻辑,将在 `handle` 方法中执行。
//`consumerTag -> {}`:表示用于接收消费者标识的回调函数。在上面的 `basicCancel` 方法中同样需要传入该回调函数中的消费者标识才能成功取消消费者的订阅。
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {
System.out.println(consumerTag);
});
}
}
}
Work 消息模型:在这种模型中,消息被发送到一个队列中,多个消费者从该队列中接收消息并按照一定的顺序进行处理。消息只能被一个消费者接收 。如果我们想要一条消息被多个消费者消费怎么办呢?就需要使用到我们的 Publish/Subscribe 模型 ,订阅模型。
可以看到,之前生产者直接对接队列
交换机做了什么呢?
特点:消息会被路由到所有绑定到该交换机的队列上。
举个场景:有10台自动售卖机,我们想给他推送广告,那么我们就可以使用这样的交换机来推送。
接下来我们看一下如何实现的。
/**
* 广播交换机:生产者不需要声明队列。它直接和交换机对接
*/
public class FanoutProducer {
private static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try(Connection connection = factory.newConnection();
Channel channel = connection.createChannel();){
//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
System.out.println("[x] Sent'"+message+"'");
}
}
}
}
/**
* 广播交换机:消费者
*/
public class FanoutConsumer {
private static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] args) throws IOException, TimeoutException {
//创建到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
Channel channel2 = connection.createChannel();
//声明交换机
channel1.exchangeDeclare(EXCHANGE_NAME,"fanout");
// 创建队列,随机分配一个队列名称
String queueName = "xiaowang_queue";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "");
String queueName2 = "xiaoli_queue";
channel2.queueDeclare(queueName2, true, false, false, null);
channel2.queueBind(queueName2, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小王] Received '" + message + "'");
};
DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小李] Received '" + message + "'");
};
channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> { });
channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> { });
}
}
绑定:可以让交换机,发送消息给某个队列,上面是全部发,这里是指定发,通过routingKey路由键。也就是,交换机也可以单独发送到指定的队列。
绑定关系:完全匹配字符串
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 orange 的消息
C2:消费者,其所在队列指定了需要routing key 为 black、green的消息
/**
* Direct交换机:
*/
public class DirectProducer {
private static final String EXCHANGE_NAME = "direct-exchange";
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try(Connection connection = factory.newConnection();
Channel channel = connection.createChannel();){
//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String userInput = scanner.next();
String[] strings = userInput.split(",");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
//之前routingKey不写的,现在定义了。
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
}
/**
* Direct交换机
*/
public class DirectConsumer {
private static final String EXCHANGE_NAME = "direct-exchange";
public static void main(String[] args)throws Exception {
//创建到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
Channel channel2 = connection.createChannel();
//声明交换机:如果交换机不存在呢?
channel1.exchangeDeclare(EXCHANGE_NAME,"direct");
// 创建队列,随机分配一个队列名称
String queueName = "xiaowang_queue1";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "xiaowang");//这里需要指定接收
String queueName2 = "xiaoli_queue1";
channel2.queueDeclare(queueName2, true, false, false, null);
channel2.queueBind(queueName2, EXCHANGE_NAME, "xiaoli");这里需要指定接收
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小王] Received '" + message + "'");
};
DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小李] Received '" + message + "'");
};
channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> { });
channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> { });
}
}
可以看到,我们这里使用上了路由键。xiaowang,xiaoli
疑问:如果路由键不指定呢??
消息会根据一个 模糊的 路由键转发到指定的队列
绑定关系:可以模糊匹配多个绑定·
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
/**
* topic交换机:
*/
public class TopicProducer {
private static final String EXCHANGE_NAME = "topic-exchange";
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try(Connection connection = factory.newConnection();
Channel channel = connection.createChannel();){
//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String userInput = scanner.next();
String[] strings = userInput.split(",");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
//之前routingKey不写的,现在定义了。
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
}
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
/**
* Direct交换机
*/
public class TopicConsumer {
private static final String EXCHANGE_NAME = "topic-exchange";
public static void main(String[] args)throws Exception {
//创建到服务器的连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
Channel channel2 = connection.createChannel();
//声明交换机:如果交换机不存在呢?
channel1.exchangeDeclare(EXCHANGE_NAME,"topic");
// 创建队列,随机分配一个队列名称
String queueName = "xiaowang_queue_topic";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "#.xiaowang.#");//这里需要指定接收
String queueName2 = "xiaoli_queue_topic";
channel2.queueDeclare(queueName2, true, false, false, null);
channel2.queueBind(queueName2, EXCHANGE_NAME, "#.xiaoli.#");这里需要指定接收
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小王] Received '" + message + "'");
};
DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小李] Received '" + message + "'");
};
channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> { });
channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> { });
}
}
可以给每条消息设置一个有效期,一段时间内未被消费者处理就过期了
场景:比如消费者没有同网络,那么这个时候随意发送的消息就会积累过多,其实这些消息当时如果没有用,后面就不需要了,那么可以设置一个有效期。分两种:给这个队列所有的消息指定过期时间和给指定消息过期时间:
在队列声明的时候,需要声明过期时间参数的设置。
// 创建队列,指定消息过期参数:但,为什么是在消费者这边设置呢?其实都可以。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
// 消费消息,会持续阻塞,取消自动ack来测试消息的过期
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
如果在过期时间内,还没有消费者取消息,消息才会过期。
注意,如果消息已经接收到,但是没确认,是不会过期的。就会出现如下这种情况,收到了但是没有确认。
如果消息处于待消费状态并且过期时间到达后,消息将被标记为过期。但是,如果消息已经被消费者消费,并且正在处理过程中,即使过期时间到达,消息仍然会被正常处理。如果重启消费者,消息也不会重新消费,直接没了???真的吗?
在发消息的时候,指定过期时间
// 给消息指定过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("1000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
为什么需要消息确认机制呢? 它确保了消息的可靠性传递和处理 。如果你发送了一条消息,比如消费者在处理过程中,出现异常,那么如果你还没确认消息接收到,消费者重新起来的时候,就可以又继续消费。服务器也知道消息被消费或其他情况,也就是需要给我一个反馈。反馈方式有三种:
代码实现:
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
第一个参数:标识消息的唯一编号,用于在手动确认消息时指定要确认的消息。
第二个参数:批量确认,表示是否一次性确认所有历史消息,包括当前这一条。false表示不批量确认
第三个参数:表示是否重新入队,可用于重试。false表示不进行
basicNack()
方法可以批量拒绝多个消息并选择是否重新入队;basicReject()
方法只能拒绝单个消息并选择是否重新入队。死信:过期的消息、拒收的消息、消息队列满了、处理失败的消息的统称
死信队列,死信交换机:都是普通的交换机,和我们平常使用的没什么区别,只是起了个名字
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.util.Scanner;
public class DlxDirectProducer {
private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";//死信队列
private static final String WORK_EXCHANGE_NAME = "direct2-exchange"; //工作队列
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, "direct");
// 1.创建死信队列,随机分配一个队列名称,创建两个队列
String queueName = "laoban_dlx_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");
String queueName2 = "waibao_dlx_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "waibao");
// 死信队列的消费者
DeliverCallback laobanDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [laoban] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback waibaoDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [waibao] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, false, laobanDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, false, waibaoDeliverCallback, consumerTag -> {
});
//2. 给工作队列发送消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String userInput = scanner.nextLine();
String[] strings = userInput.split(" ");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
channel.basicPublish(WORK_EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
}
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
public class DlxDirectConsumer {
private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";
private static final String WORK_EXCHANGE_NAME = "direct2-exchange";
/**
* 整体流程:
* 1. 创建工作队列,创建队列的时候,指定死信队列,也就是消息被解决的时候,那么就会发送给死信队列
* 2. 如果生产者发送消息给工作队列,工作队列拒绝处理,则会自动发送给死信队列
* @param argv
* @throws Exception
*/
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(WORK_EXCHANGE_NAME, "direct");
// 指定死信队列参数
Map<String, Object> args = new HashMap<>();
// 要绑定到哪个交换机
args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");
// 1.创建队列,随机分配一个队列名称
String queueName = "xiaodog_queue";
channel.queueDeclare(queueName, true, false, false, args);
channel.queueBind(queueName, WORK_EXCHANGE_NAME, "xiaodog");
Map<String, Object> args2 = new HashMap<>();
args2.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);//被解决的会放到这里面,指定的交换机
args2.put("x-dead-letter-routing-key", "laoban");//指定的routing key
// 创建队列,随机分配一个队列名称
String queueName2 = "xiaocat_queue";
channel.queueDeclare(queueName2, true, false, false, args2);
channel.queueBind(queueName2, WORK_EXCHANGE_NAME, "xiaocat");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//2. 处理消息
DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [xiaodog] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [xiaocat] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, false, xiaoyuDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, false, xiaopiDeliverCallback, consumerTag -> {
});
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: 192.168.1.103
@Configuration
public class RabbitmqConfig {
public static final String QUEUE_EMAIL = "queue_email";//email队列
public static final String QUEUE_SMS = "queue_sms";//sms队列
public static final String EXCHANGE_NAME="topic.exchange";//topics类型交换机
public static final String ROUTINGKEY_EMAIL="topic.#.email.#";
public static final String ROUTINGKEY_SMS="topic.#.sms.#";
//声明交换机
@Bean(EXCHANGE_NAME)
public Exchange exchange(){
//durable(true) 持久化,mq重启之后交换机还在
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
//声明email队列
/*
* new Queue(QUEUE_EMAIL,true,false,false)
* durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
* auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
* exclusive 表示该消息队列是否只在当前connection生效,默认是false
*/
@Bean(QUEUE_EMAIL)
public Queue emailQueue(){
return new Queue(QUEUE_EMAIL);
}
//声明sms队列
@Bean(QUEUE_SMS)
public Queue smsQueue(){
return new Queue(QUEUE_SMS);
}
//ROUTINGKEY_EMAIL队列绑定交换机,指定routingKey
@Bean
public Binding bindingEmail(@Qualifier(QUEUE_EMAIL) Queue queue,
@Qualifier(EXCHANGE_NAME) Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
}
//ROUTINGKEY_SMS队列绑定交换机,指定routingKey
@Bean
public Binding bindingSMS(@Qualifier(QUEUE_SMS) Queue queue,
@Qualifier(EXCHANGE_NAME) Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
}
}
/**
* 用于创建测试程序用到的交换机和队列(只用在程序启动前执行一次)
*/
public class BiInitMain {
public static void main(String[] args) {
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String EXCHANGE_NAME = BiMqConstant.BI_EXCHANGE_NAME;
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 创建队列,随机分配一个队列名称
String queueName = BiMqConstant.BI_QUEUE_NAME;
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, BiMqConstant.BI_ROUTING_KEY);
} catch (Exception e) {
}
}
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class Send {
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void sendMsgByTopics(){
/**
* 参数:
* 1、交换机名称
* 2、routingKey
* 3、消息内容
*/
for (int i=0;i<5;i++){
String message = "恭喜您,注册成功!userid="+i;
rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_NAME,"topic.sms.email",message);
System.out.println(" [x] Sent '" + message + "'");
}
}
}
@Component
public class ReceiveHandler {
//监听邮件队列
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "queue_email", durable = "true"),
exchange = @Exchange(
value = "topic.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"topic.#.email.#","email.*"}))
public void rece_email(String msg){
System.out.println(" [邮件服务] received : " + msg + "!");
}
//监听短信队列
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "queue_sms", durable = "true"),
exchange = @Exchange(
value = "topic.exchange",
ignoreDeclarationExceptions = "true",
type = ExchangeTypes.TOPIC
),
key = {"topic.#.sms.#"}))
public void rece_sms(String msg){
System.out.println(" [短信服务] received : " + msg + "!");
}
}
// 指定程序监听的消息队列和确认机制
@SneakyThrows
@RabbitListener(queues = {BiMqConstant.BI_QUEUE_NAME}, ackMode = "MANUAL")
public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
log.info("receiveMessage message = {}", message);
if (StringUtils.isBlank(message)) {
// 如果失败,消息拒绝
channel.basicNack(deliveryTag, false, false);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "消息为空");
}
long chartId = Long.parseLong(message);
Chart chart = chartService.getById(chartId);
if (chart == null) {
channel.basicNack(deliveryTag, false, false);
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "图表为空");
}
// 先修改图表任务状态为 “执行中”。等执行成功后,修改为 “已完成”、保存执行结果;执行失败后,状态修改为 “失败”,记录任务失败信息。
Chart updateChart = new Chart();
updateChart.setId(chart.getId());
updateChart.setStatus("running");
boolean b = chartService.updateById(updateChart);
if (!b) {
channel.basicNack(deliveryTag, false, false);
handleChartUpdateError(chart.getId(), "更新图表执行中状态失败");
return;
}
// 调用 AI
String result = aiManager.doChat(CommonConstant.BI_MODEL_ID, buildUserInput(chart));
String[] splits = result.split("【【【【【");
if (splits.length < 3) {
channel.basicNack(deliveryTag, false, false);
handleChartUpdateError(chart.getId(), "AI 生成错误");
return;
}
String genChart = splits[1].trim();
String genResult = splits[2].trim();
Chart updateChartResult = new Chart();
updateChartResult.setId(chart.getId());
updateChartResult.setGenChart(genChart);
updateChartResult.setGenResult(genResult);
// todo 建议定义状态为枚举值
updateChartResult.setStatus("succeed");
boolean updateResult = chartService.updateById(updateChartResult);
if (!updateResult) {
channel.basicNack(deliveryTag, false, false);
handleChartUpdateError(chart.getId(), "更新图表成功状态失败");
}
// 消息确认
channel.basicAck(deliveryTag, false);
}