MQ,消息队列, 队列的特征:先进先出, 一般在实战中用于上下游传递消息。比如服务A通过消息队列向服务B发送消息,供服务B消费。 在没有MQ中间件时期,大部分都是接口互调,在被调用方返回值中存放需要的数据,或者回调函数提示success,failed;
流量消峰:假设下单系统极限是100次/秒,当超过了100,服务器宕机,使用消息队列mq做缓冲,可以将100以后下单的访问请求先存入到mq中,等待被消费。 缺点:用户体验可能差一点,总比服务器挂掉好。 例子:高峰期下单时,转圈圈。
应用解耦:三个中一个出现异常, 调用方也必定异常。订单系统执行完之后会发送消息至mq, 如果三个有一个坏掉, 消息会监听,直至恢复正常. 类似于专业线业务挂掉了, 平台线的消息还放在mq里面等待消费。
异步处理:有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可以执行完,
以前解法:A 提供一个 回调notify接口,B 执行完之后调用notify通知 A 服务
MQ解法::A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此消息转发给 A 服务。
RocketMQ:底层语言是Java,阿里巴巴出品,一般应用于大型公司,毕竟承受过双11的冲击。
RabbitMQ:底层语言是Erlang,时间悠久,稳定,一般用于中小型公司。
自我感觉:没啥好大的区别,特地询问了公司的运维大佬
RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,
当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里
寄件人:产生数据发送消息的程序是生产者
收件人:消费数据的消费者
快递站点:消息队列(队列+交换机), 交换机一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个要交换机类型决定
注意:
1:交换机与队列是 一对多的关系,一个快递员可以对应多个包裹
2:每个队列可以对应多个一个消费者,不过消息只能被消费一次。
3:请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。 生产者是用户服务,消费者是订单服务,消息中间件是其他服务
每一个生产者与MQ会建议一个Connection(TCP连接), 里面有多个信道, 发送消息的通道。
Exchange : message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key(图中箭头),分发消息到 queue 中去。常用的交换机类型有:direct , topic,fanout
Queue : 消息最终被送到这里等待 consumer 取走
Binding : exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
生成者发送玩消息, 如工作队列中, 等待被消费(假设有4条消息), 不出意外的话, 会轮训被消费者消费, 且一条消息只能被一个消费者消费一次!
先看图, 再结合代码.
存在一个Name=hello的队列
生成者发送消息
两个消费者C1,C2消费消息
/**
* ClassName: RabbitMqUtils
* author: bob.ly
* Version: 1.0.0
* DateTime: 2021/07/07-22:26:00
* Description:链接工具类
*/
public class RabbitMqUtils {
public static Channel getChannel() throws Exception {
/**
* 创建链接,得到一个信道
*/
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
生成者
/**
* ClassName: Productor
* author: bob.ly
* Version: 1.0.0
* DateTime: 2021/07/07-22:28:00
* Description:
*/
public class Productor {
/**
* 声明一个队列名称
*/
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
/**
* 声明消息队列
* param1:指定队列的名称
* param2:是否持久化
* param3:是否只让一个消费者消费
* param4:是否自动删除
* param5:其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/**
* 手动从控制台当中接受信息
*/
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("发送消息完成:" + message);
}
}
}
消费者:若想main开启并行模式,将main允许一次之后, C1改成C2,再允许一次main
public class Consumer1 {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
/**
* 推送的消息如何进行消费的接口回调
* consumerTag: 消息叫什么名字
* delivery: 消息内容
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String receivedMessage = new String(delivery.getBody());
System.out.println("接收到消息:" + receivedMessage);
};
/**
* 取消回调:
*/
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
};
System.out.println("C2 消费者启动等待消费.................. ");
/**
* 消费者消费消息
* 1. 消费哪个队列
* 2. 消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3. 消费者未成功消费的回调
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
此上为理想状态, 生成者发送消息到mq中, mq一旦向消费者传递了一条消息,便立即将该消息标记为删除, 消费者消费消息. 在非正常情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答机制就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
生活场景: KFC快递员送货上门, 打开箱子, 并不是把汉堡给你了就走了, 而是要你签字, 核实完之后才离开
KFC快递员: mq
你: 消费者
签字+核实信息: 消息应答机制
箱子: 信道
消息应答机制: 自动应答 与 手动应答
api方法: channel.basicAck(用于肯定确认), 默认采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答,
若multiple为true, 说明mq将消息入信道(tag=1,2,3,4, 4条消息)给消费者时, 消费者消费tag=4时, 就自动把1,2,3也应答了.
若multiple为false: 说明消费哪个tag, 应答哪个tag
自动应答(少用): 这种模式仅适用在消费者可以在高并发情况下有效安全的进行,万一宕机了,消息便丢失了
手动应答(推荐): 可以批量应答并且减少网络拥堵, 手动返回ack(通知)告诉队列处理完了,队列进而删除消息。
消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消费者
未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者
可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确
保不会丢失任何消息。
//可以copy两份, 把C1改成C2, C2睡眠10s, 等待时间较长
public class Customer1 {
private static final String ACK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C1 等待接收消息处理时间较短");
/**
* 消息消费的时候如何处理消息
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到消息:" + message);
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
/**
* 采用手动应答
*/
channel.basicConsume(ACK_QUEUE_NAME, Boolean.FALSE, deliverCallback, (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
});
}
}
队列持久化: 之前我们创建的队列都是非持久化的,rabbitmq 如果重启的,该队列就会被删除掉,如果要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化.
/**
* 声明消息队列
* param1:指定队列的名称
* param2:********是否持久化**********, 将false改为true,则开启持久化
* param3:是否只让一个消费者消费
* param4:是否自动删除
* param5:其他参数
*/
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
消息持久化: 增加MessageProperties.PERSISTENT_TEXT_PLAIN
注意:将消息标记为持久化并不能完全保证不会丢失消息。尽管将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。
最开始分发消息采用的轮训分发,0,或者不设置都是轮训分发,这种策略并不是很好,比如有两个消费者在处理任务,消费者 1 处理任务的速度快,而消费者 2处理速度却很慢,当消费者2处理任务时(未确定ack),消费者 1 一部分时间处于空闲状态,不知道干啥。
rabbitMq并不知道消费者处理的速度效率,
避免这种情况,我们可以设置参数 channel.basicQos(1),忙的先忙,不忙的继续给任务
开启之后如下
意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个 任务,然后 rabbitmq就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的worker 或者改变其他存储任务的策略
ps:channel.basicQos(1)每个消费者都要加上。
本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止一个消息,另外来自消费
者的手动确认本质上也是异步的。这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此
缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,
RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认ack。
例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知这个情况到并再发送一条消息。消息应答和 QOS 预取值对用户吞吐量有重大影响。
参考代码案例: https://blog.csdn.net/fan521dan/article/details/104828356?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162571270816780269846930%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=162571270816780269846930&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-104828356.first_rank_v2_pc_rank_v29&utm_term=channel.basicQos&spm=1018.2226.3001.4187
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式, 所有在该信道上面发布的消
息都将会被指派一个唯一的 ID (从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会
发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,
如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
开启发布确认的方法:单个确认发布,批量确认发布,异步确认发布
发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布
确认,都需要在 channel 上调用该方法
channel.confirmSelect(); //开启消息确认
channel.waitForConfirms(); //消息确认结果,true:成功。 false:失败
单个确认发布
缺点:这是一种简单的确认方式,它是一种 同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布。waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
// 开启发布确认
channel.confirmSelect();
long begin = System.currentTimeMillis();
/**
*循环,发一个确认一个
*/
for (int i = 0; i < 50; i++){
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
// 服务端返回 false 或超时时间内未返回,生产者可以消息重发,true则表明已经到磁盘了
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println(" 消息发送成功");
}
}
long end = System.currentTimeMillis();
System.out.println(" 发布" + " 50个单独确认消息, 耗时" + (end - begin) +"ms");
批量确认发布
这种方案仍然是同步的,也一样阻塞消息的发布。与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量
.
缺点:当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。
// 开启发布确认
channel.confirmSelect();
long begin = System.currentTimeMillis();
int batchSize=100;
//分5批发送确认。
for (int i = 0; i < 500; i++){
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
// 服务端返回 false 或超时时间内未返回,生产者可以消息重发,true则表明已经到磁盘了
if(i%batchSize==0){
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println(" 消息发送成功");
}
}
}
long end = System.currentTimeMillis();
System.out.println(" 发布" + " 500个确认消息, 耗时" + (end - begin) +"ms");
异步确认发布(推荐)
/**
* @author: liuyi
* ClassName: Productor
* Version: 1.0.0
* DateTime: 2021/03/31-10:36:00
* Description:异步处理
*/
public class Productor {
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
/**
* 声明队列:暂不需要持久化。
*/
channel.queueDeclare(queueName, false, false, false, null);
/**
* 开启发布确认,确保消息mq接受到
*/
channel.confirmSelect();
/**
* 线程安全有序的一个哈希表,适用于高并发的情况
* 1. 轻松的将序号与消息进行关联
* 2. 轻松批量删除条目 只要给到序列号
* 3. 支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
/**
* 确认收到消息的一个回调
* 1. 消息序列号
* 2. true 可以确认小于等于当前序列号的消息
* false 确认当前序列号消息
*/
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
/**
* 返回的是小于等于当前序列号的未确认消息 是一个 map
*/
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(sequenceNumber, true);
/**
* 清除该部分未确认消息
*/
confirmed.clear();
} else {
/**
* 只清除当前序列号的消息
*/
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) ->
{
String message = outstandingConfirms.get(sequenceNumber);
System.out.println(" 发布的消息" + message + " 未被确认,序列号" + sequenceNumber);
};
/**
* 添加一个异步确认的监听器
* 1. 确认收到消息的回调
* 2. 未收到消息的回调
*/
channel.addConfirmListener(ackCallback, nackCallback);
for (int i = 0; i < 100; i++) {
String message = "消息" + i;
/**
* channel.getNextPublishSeqNo() 获取下一个消息的序列号
* 通过序列号与消息体进行一个关联
* 全部都是未确认的消息体
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
}
}
总结
三步必须都完成,才能确保生产者发送的消息绝对不会丢失!
待更新。
待更新。