文章目录
分析系统现状不足
现状:目前的异步是通过本地的线程池实现的
1) 无法集中限制(分布式),只能单机限制
解决方案:在一个集中的地方去管理下发任务
2) 任务由于是放在内存中执行的,可能会丢失
解决方案:把任务放在一个可以持久化存储的硬盘
3)优化:如果你的系统功能越来越多,长耗时任务越来越多,系统会越来越复杂
服务拆分(应用解耦):可以把长耗时,消耗很多的任务把它单独抽成一个程序,不要影响主业务
解决方案:可以有一个中间人,让中间人帮我们去连接两个系统(比如核心系统和智能生成业务)
中间件
连接多个系统,帮助多个系统紧密协作的技术(或者组件)
比如:Redis,消息队列,分布式存储Etcd
消息队列
什么是消息队列?
存储消息的队列。
存储:存数据
消息:某种数据结构。比如字符串,对象,二进制数据,JSON等等
队列:先进先出的数据结构
应用场景
在多个不同的系统,应用之间实现消息的传输(也可以存储),不需要考虑传输应用的编程语言,系统,框架等等
消息队列的模型
生产者,消费者,消息,消息队列
为什么不直接传输,而要用消息队列?
生产者不用关心你的消费者要不要消费,什么时候才消费,我只需要把东西给消费队列,我的工作就算完成了,生产者和消费者实现了解耦,互不影响
为什么要用消息队列?
1)异步处理
生产者发送完消息之后,可以继续去忙别的,消费者想什么时候消费都可以,不会产生阻塞
2)削峰填谷
先把用户的请求放到消息队列中,消费者可以按照自己的需求,慢慢去取
原本:12 点时来了 10 万个请求,原本情况下,10万个请求都在系统内部立刻处理,很快系统压力过大就宕机了。
现在:把这 10万个请求放到消息队列中,处理系统以自己的恒定速率(比如每秒 1 个)慢慢执行,从而保护系统、稳定处理
消息队列的缺点?
增加成本(开发,维护,部署),消息丢失,顺序性,重复消费,数据一致性(分布式系统需要考虑)
分布式消息队列
分布式消息队列的优势?
1)数据持久化:可以把消息集中存储到硬盘里,服务器重启就不会丢失
2)可扩展性:可以根据需求,随时增加(或减少)节点,继续保持稳定的服务
3)应用解耦:可以连接各个不同的语言,框架开发的系统,让这些系统能够灵活传输读取数据
应用解耦的优点:
以前,把所有功能放到同一个项目中,调用多个子功能时,一个环节出错,系统就整体出错
使用消息队列进行解耦:
- 一个系统挂了,不影响另一个系统
- 系统挂了并恢复后,仍然可以取出消息,继续执行业务
- 只要发送消息到队列,就可以立即返回,不用同步调用所有系统,性能更高
4)发布订阅:
如果一个非常大的系统要给其他子系统发送通知,最简单直接的方式是大系统直接依次调用子系统
问题:
- 每次发通知都要调用很多系统,很麻烦,有可能失败
- 新出现的项目无法得到通知
解决方案:大的核心系统始终往一个地方(消息队列)去发消息,其他的系统都去订阅这个消息队列(读取消息队列中的消息)
打个比喻,老板(发布者)要通知一件事情给员工(消费者),方式1 :给员工依次发通知;方式2 :建个群(消息队列)发通知。方式一不仅非常麻烦耗时,有时候网不好(服务器压力大时)还发不出去。方式2呢就是老板只需要把消息发送到群里(消息队列),员工打开查看即可。
消息队列应用场景
- 耗时的场景(异步)
- 高并发场景(异步,削峰填谷)
- 分布式系统协作(跨团队,跨业务协作,应用解耦)
- 强稳定性的业务(比如金融业务,持久化,可靠性,削峰填谷)
主流分布式消息队列选型
主流技术
- activemq
- rabbitmq
- kafka
- rocketmq
- zeromq
- pulsar(云原生)
- Apache InLong(Tube)
技术对比
技术选型指标:
- 吞吐量:IO,高并发
- 时效性:类似延迟,消息的发送,到达时间
- 可用性:系统可用的比率
- 可靠性:消息不丢失,功能正常完成
| 技术名称
| 吞吐量
| 时效性
| 可用性
| 可靠性
| 优势
| 应用场景
|
| — | — | — | — | — | — | — |
| activemq
| 万级
| 高
| 高
| 高
| 简单易学
| 中小型企业、项目
|
| rabbitmq
| 万级
| 极高(微秒)
| 高
| 高
| 生态好(基本什么语言都支持)、时效性高、易学
| 适合绝大多数分布式的应用,这也是先学他的原因
|
| kafka
| 十万级
| 高(毫秒以内)
| 极高
| 极高
| 吞吐量大、可靠性、可用性,强大的数据流处理能力
| 适用于 大规模处理数据的场景,比如构建日志收集系统、实时数据流传输、事件流收集传输
|
| rocketmq
| 十万级
| 高(ms)
| 极高
| 极高
| 吞吐量大、可靠性、可用性,可扩展性
| 适用于 **金融 **、电商等对可靠性要求较高的场景,适合 **大规模 **的消息处理。
|
| pulsar
| 十万级
| 高(ms)
| 极高
| 极高
| 可靠性、可用性很高,基于发布订阅模型,新兴(技术架构先进)
| 适合大规模、高并发的分布式系统(云原生)。适合实时分析、事件流处理、IoT 数据处理等。 |
RabbitMQ入门实战
基本概念
AMQP协议:https://www.rabbitmq.com/tutorials/amqp-concepts.html
定义:高级信息队列协议(Advanced Message Queue Protocol)
生产者:发消息到某个交换机
消费者:从某个队列中取信息
交换机(Exchange):负责把消息转发到对应的队列
队列(Queue):存储消息
路由(Routes):将消息从一个地方转发到另一个地方
AMQP模型:
安装
windows 安装:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.12.0
安装 erlang 25.3.2(因为 RabbitMQ 依赖 erlang,不安装这个安装RabbitMQ会报错),这个语言的性能非常高。
erlang 下载:https://www.erlang.org/patches/otp-25.3.2
安装完 erlang 后,安装 rabbitmq 即可。
win + r 打开 services.msc(服务菜单),查看 rabbitmq 服务是否已启动:
安装 rabbitmq 监控面板:
在 rabbitmq 安装目录的 sbin 中执行下述脚本:
D:\software\rabbitmq\rabbitmq_server-3.12.0\sbin
rabbitmq-plugins.bat enable rabbitmq_management
访问:http://localhost:15672,用户名密码都是 guest:
如果想要在远程服务器安装访问 rabbitmq 管理面板,你要自己创建一个管理员账号,不能用默认的 guest,否则会被拦截(官方出于安全考虑)。
如果被拦截,可以自己创建管理员用户:
参考文档的 Adding a User:https://www.rabbitmq.com/access-control.html
rabbitmq 端口占用:
5672:程序连接的端口
15672:webUI
快速入门
单向发送
一个生产者给一个队列发消息,一个消费者从这个队列取消息,一对一
引入消息队列 Java 客户端:
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.17.0</version>
</dependency>
生产者代码:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.nio.charset.StandardCharsets;
public class Send {
private final static String QUEUE_NAME = "hello";
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.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
Channel频道:理解为操作消息队列的client,提供了和消息队列server建立通信的方法(为了复用连接,提高传输效率)。程序通过channel操作rabbitmq
创建消息队列:
参数:
queueName:消息队列名称(注意,同名称的消息队列,只能用同样的参数创建一次)
durabale:消息队列重启后,消息是否丢失
exclusive:是否只允许当前这个创建消息队列的连接操作消息队列
autoDelete:没有人用队列后,是否要删除队列
执行程序后,可以看到有 1 条消息:
消费者代码:
public class SingleConsumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) 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);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 定义了如何处理消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
// 消费消息,会持续阻塞
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
启动消费者后,可以看到消息被消费了
多消费者
官方教程:https://www.rabbitmq.com/tutorials/tutorial-two-java.html
场景:多个机器同时去接受并处理任务(尤其是每个机器的处理能力有限)
一个生产者给一个队列发消息,多个消费者从这个队列中取消息
1)队列持久化
durable参数设置为true,服务器重启后队列不丢失:
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
2)消息持久化
指定MessageProperties.PERSISTENT_TEXT_PLAIN 参数:
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
生产者代码:
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 java.util.Scanner;
public class MultiProducer {
//队列名字
private static final String TASK_QUEUE_NAME = "multi_queue";
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.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.nextLine();
//在指定的交换机上发布消息到TASK_QUEUE_NAME的队列中,使用 MessageProperties.PERSISTENT_TEXT_PLAIN将消息标为持久化,最后将消息体转换为字节数组
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
消费者代码:
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;
public class MultiConsumer {
private static final String TASK_QUEUE_NAME = "multi_queue";
public static void main(String[] argv) throws Exception {
// 建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
for (int i = 0; i < 2; i++) {
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//消费者每次只能处理一个消息
channel.basicQos(1);
// 定义了如何处理消息
int finalI = i;
// 创建消息接收回调函数,以便接收消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
try {
// 处理工作
System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");
// 停 20 秒,模拟机器处理能力有限
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(" [x] Done");
// 手动发送应答,告诉RabbitMQ消息已经被处理
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
// 开始消费消息,传入队列名称,是否自动确认,投递回调和消费者取消回调
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {
});
}
}
}
交换机
**交换机 **是消息队列中的一个组件,其作用类似于网络路由器。它负责将我们发送的消息转发到相应的目标,就像快递站将快递发送到对应的站点,或者网络路由器将网络请求转发到相应的服务器或客户端一样。交换机的主要功能是提供转发消息的能力,根据消息的路由规则将消息投递到合适的队列或绑定的消费者。
我们可以理解为,如果说一个快递站已经承受不了那么多的快递了,就建多个快递站。
fanout
扇出,广播
特点:消息会被转发到所有绑定到该交换机的队列
场景:很适用于发布订阅的场景,比如写日志,可以多个系统间共享
示例场景:
生产者代码:
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class FanoutProducer {
// 交换机名字
private static final String EXCHANGE_NAME = "fanout-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(EXCHANGE_NAME, "fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
消费者代码:
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;
public class FanoutConsumer {
//交换机名字
private static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] argv) throws Exception {
//建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
//创建频道
Channel channel1 = connection.createChannel();
// 声明交换机
channel1.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 创建队列1,连接到交换机上
String queueName = "xiaowang_queue";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "");
// 创建队列2,连接到交换机上
String queueName2 = "xiaoli_queue";
channel1.queueDeclare(queueName2, true, false, false, null);
channel1.queueBind(queueName2, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 创建交付回调函数1
DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小王] Received '" + message + "'");
};
// 创建交付回调函数2
DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小李] Received '" + message + "'");
};
// 开始消费消息队列1
channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> { });
// 开始消费消息队列2
channel1.basicConsume(queueName2, true, deliverCallback2, consumerTag -> { });
}
}
Direct
官方教程:https://www.rabbitmq.com/tutorials/tutorial-four-java.html
特点:消息会根据路由键转发到指定的队列
场景:特定的消息只交给特定的系统(程序)来处理
注意:不同队列可以绑定相同的路由键
示例场景:
老板在发送消息同时会带上路由键,根据路由键找对应的队列来发送
生产者代码:
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class DirectProducer {
private static final String EXCHANGE_NAME = "direct-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()) {
//声明交换机是direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//输入消息 和 路由键
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(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;
public class DirectConsumer {
private static final String EXCHANGE_NAME = "direct-exchange";
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(EXCHANGE_NAME, "direct");
// 创建队列
String queueName = "xiaoyu_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "xiaoyu"); //指定2交换机和路由键
// 创建队列,随机分配一个队列名称
String queueName2 = "xiaopi_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, EXCHANGE_NAME, "xiaopi");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoyu] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaopi] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, xiaoyuDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, true, xiaopiDeliverCallback, consumerTag -> {
});
}
}
topic
官方教程:https://www.rabbitmq.com/tutorials/tutorial-five-java.html
特点:消息会根据一个模糊的路由键转发到指定的队列
场景:特定的一类消息可以交给特定的一类系统(程序)来处理
规则:
- :匹配一个单词,比如.orange,那么 abc.orange、ikun.orange 都能匹配
- #:匹配0个或多个单词,比如orange.#,那么orange,orange.abc.ikun都能匹配
应用场景:
老板要下发一个任务,让多个组来处理
生产者代码:
package com.yupi.springbootinit.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class TopicProducer {
private static final String EXCHANGE_NAME = "topic-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(EXCHANGE_NAME, "topic");
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(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;
public class TopicConsumer {
private static final String EXCHANGE_NAME = "topic-exchange";
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(EXCHANGE_NAME, "topic");
// 创建队列
String queueName = "frontend_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "#.前端.#");
// 创建队列
String queueName2 = "backend_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");
// 创建队列
String queueName3 = "product_queue";
channel.queueDeclare(queueName3, true, false, false, null);
channel.queueBind(queueName3, EXCHANGE_NAME, "#.产品.#");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaoaDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoa] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaobDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaob] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaocDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoc] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, xiaoaDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, true, xiaobDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName3, true, xiaocDeliverCallback, consumerTag -> {
});
}
}
这样生产者发消息:前端.后端
就可以匹配到前端和后端两个队列
Headers
可以根据headers中的内容来指定发送到哪个队列,由于性能差,比较复杂,一般不推荐使用
RPC
支持用消息队列来模拟RPC的调用,但是一般没必要,直接用 Dubbo、GRPC 等 RPC 框架就好了。
核心特性
消息过期机制
官方文档:https://www.rabbitmq.com/ttl.html
可以给每条消息指定一个有效期,一段时间内未被消费者处理,就过期了
适用场景:清理过期数据
1)给队列中的所有消息指定过期时间
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
如果在过期时间内,还没有消费者取消息,消息才会过期
注意,如果消息已经接收到,但是没确认,是不会过期的
消费者中给队列中所有消息设置过期时间:
public class TtlConsumer {
private final static String QUEUE_NAME = "ttl_queue";
public static void main(String[] argv) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建队列,指定消息过期参数
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 定义了如何处理消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
// 消费消息,会持续阻塞
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
}
2)给某条消息指定过期时间
// 给消息指定过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("1000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
生产者给某条消息指定过期时间
public class TtlProducer {
private final static String QUEUE_NAME = "ttl_queue";
public static void main(String[] argv) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
// factory.setUsername();
// factory.setPassword();
// factory.setPort();
// 建立连接、创建频道
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 发送消息
String message = "Hello World!";
// 给消息指定过期时间
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 + "'");
}
}
}
消息确认机制
官方文档:https://www.rabbitmq.com/confirms.html
为保证消息成功被消费,rabbitmq提供了消息确认机制,当消费者收到消息后要给一个成功反馈:
●ack:消费成功
●nack:消费失败
●reject:拒绝
如果告诉 rabbitmq 服务器消费成功,服务器才会放心地移除消息。
支持配置 autoack,会自动执行 ack 命令,接收到消息立刻就成功了。
channel.basicConsume(queueName, true, xiaoyuDeliverCallback, consumerTag -> {
});
一般情况,建议 autoack 改为 false,根据实际情况,去手动确认。
指定确认某条消息:
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
第二个参数 multiple 批量确认:是指是否要一次性确认所有的历史消息直到当前这条消息
指定拒绝某条消息:
channel.basicNack(delivery.getEnvelope().getDeliveryTag(),false,false);
第 3 个参数表示是否重新入队,可用于重试
死信队列
官方文档:https://www.rabbitmq.com/dlx.html
为了保证消息的可靠性,比如每条消息都成功消费,需要提供一个容错机制,即:失败的消息怎么处理?
死信:过期的消息,拒收的消息,消息队列满了,处理失败的消息的统称
死信队列:专门处理死信的队列
死信交换机:专门给死信队列转发消息的交换机
示例场景:
实现:
1)创建死信交换机和死信队列,并且绑定关系
2)给失败之后需要容错处理的队列绑定死信交换机
3)可以给要容错的队列指定死信之后的转发规则,死信应该再转发到哪个死信队列
4)可以通过程序来读取死信队列中的消息,从而进行处理
生产者代码:
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");
// 创建laoban死信队列
String queueName = "laoban_dlx_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");
//创建waibao死信队列
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 -> {
});
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";
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");
//小狗的死信要转发到waibao这个死信队列
// 指定死信队列参数
Map<String, Object> args = new HashMap<>();
// 要绑定到哪个交换机
args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");
// 创建队列,随机分配一个队列名称
String queueName = "xiaodog_queue";
channel.queueDeclare(queueName, true, false, false, args);
channel.queueBind(queueName, WORK_EXCHANGE_NAME, "xiaodog");
//小猫的死信要转发到laoban这个死信队列
Map<String, Object> args2 = new HashMap<>();
args2.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
args2.put("x-dead-letter-routing-key", "laoban");
// 创建队列,随机分配一个队列名称
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");
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 -> {
});
}
}
RabbitMQ项目实战
选择客户端
怎么在项目中使用RabbitMQ?
- 使用官方的客户端(类比jdbc)
优点:兼容性好,换语言成本低,比较灵活
缺点:太灵活,要自己去处理一些事情,比如要自己维护管理链接,很麻烦
- 使用封装好的客户端,比如Spring Boot RabbitMQ Starter(类比mybatis)
优点:简单易用,直接配置直接用,更方便地去管理链接
缺点:不够灵活,被框架限制
基础实战
我们使用Spring Boot RabbitMQ Starter
https://spring.io/guides/gs/messaging-rabbitmq/
- 引入依赖
注意:使用的版本一定要和你的springboot版本一致,去maven中心仓库中找版本一致的
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.2</version>
</dependency>
2) 在yml中引入配置
spring:
rabbitmq:
host: localhost
port: 5672
password: guest
username: guest
3)创建交换机和队列,一般在项目启动之前执行创建一次即可
package com.yupi.springbootinit.bimq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 用于创建测试程序用到的交换机和队列(只用在程序启动前执行一次)
*/
public class MqInitMain {
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 = "code_exchange";
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
String queueName = "code_queue";
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,EXCHANGE_NAME,"my_routingKey");
}catch (Exception e){
}
}
}
4)生产者代码
package com.yupi.springbootinit.bimq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class MyMessageProducer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendMessage(String exchange,String routingKey,String message){
rabbitTemplate.convertAndSend(exchange,routingKey,message);
}
}
5)消费者代码
package com.yupi.springbootinit.bimq;
import com.rabbitmq.client.Channel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MyMessageConsumer {
//使用@SneakyThrows注解简化异常处理
//使得你可以在不声明抛出异常的方法中抛出受检异常,而无需捕获它们。这在一些特定情况下可能会很有用,但通常不建议频繁使用,因为它可能会破坏代码的可读性和健壮性。
@SneakyThrows
//使用@RabbitListener注解指定要监听的队列名称为"code_queue",并设置消息的确认机制为手动确认
@RabbitListener(queues = {"code_queue"},ackMode = "MUNAL")
// // 在RabbitMQ中,每条消息都会被分配一个唯一的投递标签,用于标识该消息在通道中的投递状态和顺序。通过使用@Header(AmqpHeaders.DELIVERY_TAG)注解,可以从消息头中提取出该投递标签,并将其赋值给long deliveryTag参数。
public void reciveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliverttag){
log.info("receiveMessage message = {}", message);
//手动确认消息的接收
channel.basicAck(deliverttag,false);
}
}
测试类测试
package com.yupi.springbootinit.bimq;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class MyMessageProducerTest {
@Resource
private MyMessageProducer myMessageProducer;
@Test
void sendMessage() {
myMessageProducer.sendMessage("code_exchange", "my_routingKey", "你好呀");
}
}
打印出了日志,说明消费者收到了消息,测试通过!
BI项目改造
以前把任务提交到线程池,然后在线程池提交中编写处理程序的代码,线程池内排队。
如果程序中断了,任务就没了,就丢了。
改造后的流程:
- 把任务提交改为向队列发送消息
- 写一个专门接收消息的程序,处理任务
- 如果程序中断了,消息未被确认,还会重发
- 现在,消息全部集中发送到消息队列,你可以部署多个后端,都从同一个地方取任务,从而实现了分布式负载均衡
实现步骤
1)创建交换机和队列
2)将线程池中的执行代码移到消费者类中
3)根据消费者的需求来确认消息的格式(chartId)
4)将提交线程池改造为发送消息到队列
注意:如果程序中断了,没有ack,也没有nack(服务中断,没有任何响应),那么这条消息会被重新放到消息队列中,从而实现了每个任务都会执行
记得在项目启动前创建队列和交换机
package com.yupi.springbootinit.bimq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 用于创建测试程序用到的交换机和队列(只用在程序启动前执行一次)
*/
public class BiMqInitMain {
public static void main(String[] args) {
try{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(BiMqConstant.BI_EXCHANGE_NAME,"direct");
String queueName = BiMqConstant.BI_QUEUE_NAME;
channel.queueDeclare(queueName,true,false,false,null);
channel.queueBind(queueName,BiMqConstant.BI_EXCHANGE_NAME,BiMqConstant.BI_ROUTING_KEY);
}catch (Exception e){
}
}
}
生产者:
package com.yupi.springbootinit.bimq;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class BiMessageProducer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendMessage(String message){
// 使用rabbitTemplate的convertAndSend方法将消息发送到指定的交换机和路由键
rabbitTemplate.convertAndSend(BiMqConstant.BI_EXCHANGE_NAME,BiMqConstant.BI_ROUTING_KEY,message);
}
}
消费者:
package com.yupi.springbootinit.bimq;
import cn.hutool.core.text.StrBuilder;
import com.rabbitmq.client.Channel;
import com.yupi.springbootinit.common.ErrorCode;
import com.yupi.springbootinit.constant.CommonConstant;
import com.yupi.springbootinit.exception.BusinessException;
import com.yupi.springbootinit.manager.AiManager;
import com.yupi.springbootinit.model.entity.Chart;
import com.yupi.springbootinit.service.ChartService;
import com.yupi.springbootinit.utils.ExcelUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import javax.annotation.RegEx;
import javax.annotation.Resource;
@Component
@Slf4j
public class BiMessageConsumer {
@Resource
private ChartService chartService;
@Resource
private AiManager aiManager;
//使用@SneakyThrows注解简化异常处理
//使得你可以在不声明抛出异常的方法中抛出受检异常,而无需捕获它们。这在一些特定情况下可能会很有用,但通常不建议频繁使用,因为它可能会破坏代码的可读性和健壮性。
@SneakyThrows
//使用@RabbitListener注解指定要监听的队列名称为"code_queue",并设置消息的确认机制为手动确认
@RabbitListener(queues = {BiMqConstant.BI_QUEUE_NAME},ackMode = "MANUAL")
// // 在RabbitMQ中,每条消息都会被分配一个唯一的投递标签,用于标识该消息在通道中的投递状态和顺序。通过使用@Header(AmqpHeaders.DELIVERY_TAG)注解,可以从消息头中提取出该投递标签,并将其赋值给long deliveryTag参数。
public void reciveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliverttag){
log.info("receive message = {}" + message);
if (StringUtils.isBlank(message)) {
//拒绝消息
channel.basicNack(deliverttag,false,false);
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"消息为空");
}
long chartId = Long.parseLong(message);
Chart chart = chartService.getById(chartId);
if(chart == null){
channel.basicNack(deliverttag,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){
handleChartUpdateError(chart.getId(),"更新图表执行中状态失败");
return;
}
String result = aiManager.doChat(CommonConstant.BI_MODEL_ID, getUserInput(chart));
String[] splits = result.split("【【【【【");
if(splits.length < 3){
handleChartUpdateError(chart.getId(),"AI生成错误");
return;
}
String genChart = splits[1];
String genResult = splits[2];
Chart updateChart2 = new Chart();
updateChart2.setId(chart.getId());
updateChart2.setStatus("succeed");
updateChart2.setGenChart(genChart);
updateChart2.setGenResult(genResult);
boolean b1 = chartService.updateById(updateChart2);
if(!b1){
handleChartUpdateError(chart.getId(),"更新图表成功状态失败");
return;
}
//确认消息
channel.basicAck(deliverttag,false);
}
/**
* 根据chart获取用户的输入
* @param chart
* @param
*/
public String getUserInput(Chart chart){
String goal = chart.getGoal();
String chartType = chart.getChartType();
String CSVData = chart.getChartData();
StrBuilder userInput = new StrBuilder();
userInput.append("分析需求:").append("\n");
String userGoal = goal;
if(StringUtils.isNotBlank(chartType)){
//指定了图表类型,就在目标上拼接请使用,图表类型
userGoal += "请使用," + chartType;
}
userInput.append(CSVData).append("\n");
return userInput.toString();
}
public void handleChartUpdateError(long chartId,String execMessage){
Chart chart = new Chart();
chart.setId(chartId);
chart.setStatus("failed");
chart.setExecMessage(execMessage);
boolean b = chartService.updateById(chart);
if(!b){
log.error("更新图表失败状态失败" + chartId + "," + execMessage);
}
}
}