1.介绍
消息队列主要能实现三种功能:
1.服务解耦
2.流量削峰
3.异步调用
AMQP协议:高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计,兼容jms
JMS:消息服务的标准或者是规范
2.Rabbitmq基本原理和概念
地址:RabbitMQ: One broker to queue them all | RabbitMQ
基于AMQP协议实现的消息队列,它是一种应用程序之间的通信方法
原理:
producer
消息生产者,通过信道channel发送到rabbitmq
connection
连接对象
producer和broker,broker和consumer之间的连接采用TCP,connection连接对象创建信道进行通信
channel
信道,消息传递数据的通道
broker
可以认为是mq
消息队列服务的进程,此进程包括两个部分:exchange交换机和queue队列
exchange交换机
rabbitmq非常重要的组件,接受来自于生产者的消息,推送消息到队列中
queue队列
rabbitmq内内部中的数据结构队列,是消息真正存储的地方。
consumer
消费者,消费方客户端。通过信道channel接受mq消息,并进行相关处理。
发送消息:消费者通过connection和broker建立TCP连接,connection建立channel信道,生产者通过信道,将消息发送给broker,由exchange将消息进行转发到队列中去
消费消息:消费者通过connection和broker 建立tcp连接,connection建立信道,消费者监听指定的queue队列,当有消息到达时候,通过channel推送给消费者。
routingkey
路由键,是用于将消息路由到指定队列的关键字
exchange交换机四种类型:
rabbitmq核心思想,生产者永远不会将消息直接发送到队列,而是发送到交换机,交换机将消息发送到队列
fanout
广播模式,交换机将消息发送给所有的队列,发送相同的消息。
direct
路由模式,根据对应routing key发送消息
topic
动态路由模式。通过规则定义routing key 使交换机动态的多样性选择队列
headers
请求头模式,像请求头一样附带头部数据,交换机根据头部数据匹配对应的队列
3.安装
下面直接展示docker容器安装
bash
#在线安装
docker pull rabbitmq:management
#使用官方定义的端口号启动
docker run -d -it --rm --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.13-management
java
# 查询rabbitmq容器ID
docker ps
# 进入容器
docker exec -it 容器ID /bin/bash
# 开启管控台插件
rabbitmq-plugins enable rabbitmq_management
访问15762端口,能否出现登录界面,如果出现则表示启动成功。
4.应用
简单应用
一个生产者,默认交换机,一个队列一个消费者
Java连接rabbitmq
java
public Connection getConnection () {
Connection connection = null;
// 获取链接
String HOST=EidConfigurer.get("rabbitmq.host");
String VIRTUALHOST=EidConfigurer.get("rabbitmq.virhost");
String USERNAME=EidConfigurer.get("rabbitmq.username");
String PASSWORD=EidConfigurer.get("rabbitmq.password");
String PORT=EidConfigurer.get("rabbitmq.port");
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(HOST);
connectionFactory.setVirtualHost(VIRTUALHOST);
connectionFactory.setUsername(USERNAME);
connectionFactory.setPassword(PASSWORD);
// rabbitmq 的服务器地址 15672:给rabbitmq management web程序,插件 web端客户端管理工具
//5672:给rabbitmq-server 服务器的
connectionFactory.setPort(Integer.valueOf(PORT));
// 建立链接
try {
Connection newConnection = connectionFactory.newConnection();
connection = newConnection;
} catch (IOException e) {
System.out.println("连接失败!!!");
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("连接超时!!!");
e.printStackTrace();
}
return connection;
}
发送消息
java
Connection connection = RabbitConfig.getConnection();
Channel channel = connection.createChannel();
String msg = "测试发布";
/**
* 参数1:指定exchange,使用""。默认的exchange
* 参数2:指定路由的规则,使用具体的队列名称。exchange为""时,消息直接发送到队列中
* 参数3:制动传递的消息携带的properties
* 参数4:指定传递的消息,byte[]类型
*/
channel.basicPublish("", "test1", null,msg.getBytes());
channel.close();
connection.close();
消费消息
java
Connection connection = RabbitConfig.getConnection();
Channel channel = connection.createChannel();
/**
* 参数1:queue 指定队列名称
* 参数2:durable 是否开启持久化(true)
* 参数3:exclusive 是否排外
* 参数4:autoDelete 如果这个队列没有其他消费者在消费,队列自动删除
* 参数5:arguments 指定队列携带的信息
*
*/
channel.queueDeclare("test1",true,false,false,null);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//业务代码
}
};
/**
* 参数1:queue 指定消费哪个队列
* 参数1:deliverCallback 指定是否ACK(true:收到消息会立即告诉rabbitmq,false:手动告诉)
* 参数1:cancelCallback 指定消费回调
*
*/
channel.basicConsume("test1",true,consumer);
channel.close();
connection.close();
整合springboot
需要引入依赖
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件
java
spring:
rabbitmq:
host: 39.107.111.12
virtual-host: /
username: username
password: username
port: 5672
创建配置
java
@Configuration
public class DirectExchangeConfig {
@Bean
public DirectExchange directExchange(){
DirectExchange directExchange=new DirectExchange("direct");
return directExchange;
}
@Bean
public Queue directQueue1() {
Queue queue=new Queue("directqueue1");
return queue;
}
@Bean
public Queue directQueue2() {
Queue queue=new Queue("directqueue2");
return queue;
}
//3个binding将交换机和相应队列连起来
@Bean
public Binding bindingorange(){
Binding binding=BindingBuilder.bind(directQueue1()).to(directExchange()).with("orange");
return binding;
}
@Bean
public Binding bindingblack(){
Binding binding=BindingBuilder.bind(directQueue2()).to(directExchange()).with("black");
return binding;
}
@Bean
public Binding bindinggreen(){
Binding binding=BindingBuilder.bind(directQueue2()).to(directExchange()).with("green");
return binding;
}
}
接收消息
java
@Component
public class Consumer {
@RabbitListener(queues = "directqueue1")
public void getMassage(Object massage){
业务代码
}
}
5.ack机制
消息一旦被消费者接收,队列中的消息就会被删除。
自动ack:消息一旦被接收,消费者自动发送ack,默认此模式。
消费者高效处理消息的情况下才会用此模式
手动ack:消息接收后,不会发送ack,需要代码调用
ack调用代码:
channel.basicAck(long deliveryTag,boolean multiple);//用于肯定确认
deliveryTag:参数标识,
multiple:批处理
channel.basicNack(long deliveryTag, boolean false, boolean requeue); //用于否定确认
deliveryTag:参数标识,
multiple:批处理
requeue:true重回队列,false丢弃,或者进入死信队列
channel.basicReject(long deliveryTag, boolean requeue); //用于否定确认 不进行批处理
一般消息不用批处理,所以这里不展开讨论
6.持久化
消息持久化
channel.basicPublish("交换机", "队列名", MessageProperties.PERSISTENT_TEXT_PLAIN, "消息");
队列持久化
// 让队列持久化
boolean durable = true; // false 不持久化 true 持久化
// 声明
channel.queueDeclare("队列名", durable, false, false, null);
7.分发机制
轮训分发
轮训分发是 RabbitMQ 的默认消息分发策略。它将消息按顺序均匀地分发给每个消费者,而不考虑每个消费者的处理能力和处理速度。这种策略的优点是实现简单,能够在消费者处理能力均衡的情况下提供较好的性能。然而,在消费者处理能力不均衡的情况下,轮训分发可能导致一些消费者过载,而另一些消费者闲置,降低系统的整体效率
公平分化
公平分发是一种更为智能的消息分发策略,它根据每个消费者的处理能力和处理速度来分配消息,确保处理能力强的消费者能够处理更多的消息,从而提高整体系统的效率。公平分发通过配置 prefetch 值来实现,该值定义了 RabbitMQ 在接收消费者的 ack(确认)之前可以发送给该消费者的最大消息数。通过设置较小的 prefetch 值,可以确保每个消费者在处理完当前消息之前不会收到更多的消息,从而实现消息的公平分发
不公平分发
信道设置:channel.basicQos(1);
预取值分发
和不公平分发类似
channel.basicQos(X);X>1.
8.confirm机制
将信道设置为confirm模式,所有在该信道上面发布消息都将会为消息指派一个消息id,且唯一
生产者将消息发送给broker,broker收到消息后就会发送一个确认confirm给生产者,消息达到队列
如果消息进行了持久化设置,那么消息写入磁盘后会发送一个confirm给生产者。
发布策略确认:
rabbitmq是没有设置发布确认机制,如果想要开启,需要在信道channel上面开启
java
channel.confirmSelect();
单个发布确认:以同步方式发送。优点可靠,缺点很慢,如果数据量很大,不建议。
批量发布确认:同样以同步方式发送,增加了批处理的规则。优点,时间比单个发布确认耗时少,缺点是当某个消息未送达到broker,无法准确知晓是哪个消息。
异步发布确认:比上面两个来说都复杂,但性能和效率提升不少。
原理:
发送者每次给broker发送消息,会默认给每个消息带上一个唯一的id标识
发送者在方法中写一个异步确认监听器addConfirmListener,用于监听broker给生产者发送消息
发送者根据addConfirmListener(ack,nack)来处理业务
java
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
/** 编写回调监听器,因为: 消息发送出错要立刻进行监听所以,所以创建在发送消息之前; **/
/** ack 确认收到消息的一个回调 1.消息序列号 2.true 批量确认接受小于等于当前序列号的数据 false 确认当前序列号消息 */
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
System.out.println("消息成功接收:"+sequenceNumber);
// ConcurrentNavigableMap方法()返回的是小于|等于 K 的集合, true:小于等于 false:返回小于该序列号的数据集合;
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(sequenceNumber, true);
// 清除该部分确认消息 confirmed 里保存的都是,MQ 已经接收的消息;
// 遍历 confirmed K, 根据 K 删除 outstandingConfirms 的值...
// outstandingConfirms 里面保存的都是,MQ 还未确认的消息...
}else{
//只清除当前序列号的消息
outstandingConfirms.remove(sequenceNumber);
}
};
// nack 消息失败执行{} 可以写,消息失败需要执行的代码...
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
// 这里就输出一下为被确认的消息...
String message = outstandingConfirms.get(sequenceNumber);
System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
};
// 发生者 等待MQ回调消息确认的 监听器, 本次程序值监听 ack成功的消息;
channel.addConfirmListener(ackCallback, null);
- 发送者,只需要关注消息的发送,MQ 会将每条消息发布情况,回调给发生者
- 发送者每次发消息前,将消息存储在缓存中,成功了就删除,失败了就重新发送
9.交换机
前面简单介绍过交换机,增加补充一点:topic
动态路由模式:*表示一个单词,#表示任意数量的单词,0个或者多个
发布订阅模式:fanout
最常见的场景:群聊
定义一个生产者,交换机,生产者不停往交换机发消息,交换机提前与一个或者多个队列进行绑定
生产者发布消息
java
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
消费者1接收代码
java
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/** 生成一个临时的队列 队列的名称是随机的 当消费者断开和该队列的连接时 队列自动删除 */
String queueName = channel.queueDeclare().getQueue();
// 绑定: 把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串: Fanout模式 routingkey 没作用!
channel.queueBind(queueName, EXCHANGE_NAME, "");
// 发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("wsm 发布的最新消息:"+message);
};
// 消费者监听消息
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
消费者2和消费者1代码一样。
路由模式:direct
绑定交换机需要指定routing key,一个队列可以绑定一个或者多个routingkey
生产者
java
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/** 发送消息 **/
channel.basicPublish(EXCHANGE_NAME, "key1", null, "key1发送的消息".getBytes("UTF-8"));
channel.basicPublish(EXCHANGE_NAME, "key2", null, "key2发送的消息".getBytes("UTF-8"));
消费者1
java
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/** 生成一个临时的队列 队列的名称是随机的 当消费者断开和该队列的连接时 队列自动删除 */
String queueName = channel.queueDeclare().getQueue();
// 绑定: 把该临时队列绑定我们的 exchange
##############################区别代码###########
// 参数二 设置该队列和交换和绑定的 routingkey
channel.queueBind(queueName, EXCHANGE_NAME, "key1");
##############################区别代码###########
// 发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("最新的消息是:"+message);
};
// 消费者监听消息
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
消费者2
java
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/** 生成一个临时的队列 队列的名称是随机的 当消费者断开和该队列的连接时 队列自动删除 */
String queueName = channel.queueDeclare().getQueue();
// 绑定: 把该临时队列绑定我们的 exchange
####################################################################
// 参数二 设置该队列和交换和绑定的 routingkey
channel.queueBind(queueName, EXCHANGE_NAME, "key1");
channel.queueBind(queueName, EXCHANGE_NAME, "key2");
####################################################################
// 发送回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("最新的消息是:"+message);
};
// 消费者监听消息
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {});
这样消费者2就能接收key1 和key2 的消息
主题模式(也叫动态路由):topic
动态路由模式 可以根据一些特殊的符合匹配多种 Routingkey 的匹配
#:匹配0个或者多个:sensormsg.#=sensormsg.com sensormsg.com.aa sensormsg.c.s.v
*:匹配一个:sensormsg.*=sensormsg.com sensormsg.a sensormsg.b
头部模式:headers
不常用,不做介绍