1.什么是工作队列:
- 1.工作队列模式又称之为任务队列模式,其主要思想是为了
避免
立即执行资源密集型任务,而不得不等待消费端它完成消息的消费再继续处理消息的这种情况产生。相反的是我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列
。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程
(就是指的消费者)时,这些工作线程将一起处理这些任务
生产者大量发消息,导致大量消息在队列中积累,如果有一个线程处理消息太慢,为了尽快处理消息,所以就有许多线程来进行消息的处理
2.编码实现工作队列模式:
生产端的每个消息如果只能被处理一次,不可以处理多次;所以每个消息只能被一个消费者消费,所以MQ
就把所有的消息轮着分别分发给三个线程,进行轮训分发消息:
。线程之间是竞争关系
2.1.抽取工具类:
- 1.工具类是
RabbitMqUtils .java
,这样就可以避免重复编写与RabbitMQ连接,获取信道的时候,就在需要的时接直接调用工具类即可:package com.jianqun.rabbitmq.util; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * 此类为连接工厂创建信道的工具类 * */ public class RabbitMqUtils { //得到一个连接的 channel public static Channel getChannel() throws Exception{ //创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.148.3"); factory.setUsername("admin"); factory.setPassword("123"); Connection connection = factory.newConnection();//创建连接 Channel channel = connection.createChannel();//创建信道 return channel; } }
2.2.模拟多个工作线程:
- 1.这里我们就以启动两个工作线程来模拟有多个消费线程,其消费者代码如下:
package com.jianqun.rabbitmq.two; import com.jianqun.rabbitmq.util.RabbitMqUtils; import com.rabbitmq.client.*; /** * 消费者1 * 这是一个工作线程,相当于简单模式中的消费者 * 为了 测试工作队列 */ public class Worker01 { //队列的名称 private final static String QUEUE_NAME = "hello"; public static void main(String[] args) throws Exception { //从连接工厂中获取所创建的信道 Channel channel1 = RabbitMqUtils.getChannel(); System.out.println("C2等待接受消息"); //推送的消息如何进行消费的接口回调 DeliverCallback deliverCallback=(consumerTag, delivery)->{ String message= new String(delivery.getBody()); System.out.println(message); }; //取消消费的一个回调接口 如在消费的时候队列被删除掉了 CancelCallback cancelCallback=(consumerTag)->{ System.out.println("消息消费被中断"); }; /** * 消费者消费消息 * 1.消费哪个队列 * 2.消费成功之后是否要自动应答,true代表自动应答,false手动应答 * 3.消费者未成功消费的回调 * 4.消费者取消消费的回调 */ channel1.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback); } }
- 2.通过如下设置IDEA(2022.2版本),再次运行
Worker01.java
,就可以启动第二个线程:
2.3.编码实现发送线程任务:
- 1.发送线程任务就是生产者,进行发送消息的:
/** * 生产者 * 可以发送大量消息 * */ public class Task01 { private static final String QUEUE_NAME="hello"; public static void main(String[] args) throws Exception { Channel channel=RabbitMqUtils.getChannel(); /** * 生成一个队列,其参数的含义如下所示: * 1.队列名称 * 2.队列里面的消息是否持久化 默认消息存储在内存中,true代表持久化 * 3.该队列是否只供一个消费者进行消费 是否进行共享,true可以多个消费者消费 * 4.是否自动删除,最后一个消费者端开连接以后,该队列是否自动删除,true自动删除 * 5.其他参数 */ 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); } } }
2.4.测试工作队列模式:
- 1.如下可以看到,在工作队列模式中,两个消息消费者
轮询
进行消息的消费,确保了每个消息只能消费1次
3.消息应答机制
3.1.什么是消息应答:
- 1.消息应答就是:在消费端消费完消息后要告诉生产端,消息已经处理完了,rabbitmq 可以把该消息删除了,这种机制主要是为了防止消息丢失,
消费者完成一个任务可能需要一段时间,如果其中一个
消费者处理一个长的任务并仅只完成了部分突然它挂掉了
的情况时。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除
。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制
3.2.消息应答的分类:
a. 自动应答
- 1.消息发送后立即被认为已经传送成功,这种模式需要在
高吞吐量和数据传输安全性方面
做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了。 - 2.另一方面费者没有对传递的消息数量进行限制 可能会造成消费者接受到过载的消息,可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以
这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用
b. 手动应答
b1.手动应答消息的方法:
- 1.
Channel.basicAck(用于肯定确认)
:RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了 - 2.
Channel.basicNack(用于否定确认)
: - 3.
Channel.basicReject(用于否定确认)
与 Channel.basicNack 相比
少一个参数(少的参数就是Multiple ,批量处理参数)
,不处理该消息了直接拒绝,可以将其丢弃了
b2. Channel.basicNack()否定应答中的参数Multiple 的解释:
手动应答的好处
是可以批量应答并且减少网络拥堵
multiple 的 true 和 false
代表不同意思true 代表批量应答channel上未应答的消息
:比如说 channel 上有传送 tag 的消息 5,6,7,8
. 当前 tag 是 8 那么此时5-8 的这些还未应答的消息都会被确认收到消息应答;false 同上面相比只会应答 tag=8 的消息 ,5,6,7 这三个消息依然不会被确认收到消息应答
4.消息自动重新入队
4.1.什么是重新入队:
- 1.如果消费者
由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认
,RabbitMQ 将了解到消息未完全处理
,并将对其重新排队
- 2.如果此时其他消费者可以处理,它将很快
将其重新分发给另一个消费
者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息
4.2.消息重新入队的过程图解:
5.编码实现手动应答:
5.1.手动应答逻辑图:
- 1.默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答:
5.2.编码实现手动应答:
a.生产者编码实现:
- 1.这个是消息的发送端的代码,是生产消息的
package com.jianqun.rabbitmq.three;
import com.jianqun.rabbitmq.util.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 生产者:为了测试手动应答
*
*/
public class Task02 {
//定义一个属性,后面作为队列的名字
private final static String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
//使用工具类,获取到信道,使用信道来发送消息内容
Channel channel2 = RabbitMqUtils.getChannel();
/**
* 生成一个队列,其参数的含义如下所示:
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中,true代表持久化
* 3.该队列是否只供一个消费者进行消费 是否进行共享,true可以多个消费者消费
* 4.是否自动删除,最后一个消费者端开连接以后,该队列是否自动删除,true自动删除
* 5.其他参数
*/
//声明一个队列
channel2.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
/**
* 从控制台中接受信息
*/
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
/**
* 发送一个消息
* 1.发送到哪个交换机
* 2.路由的 key 是哪个,这里使用队列的名称
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel2.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
b.消费端编码实现:
b1.睡眠工具类:
package com.jianqun.rabbitmq.util;
public class SleepUtils {
public static void sleep(int second){
try {
Thread.sleep(1000*second);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
b2.消费者1:睡眠1秒后处理消息
package com.jianqun.rabbitmq.three;
import com.jianqun.rabbitmq.util.RabbitMqUtils;
import com.jianqun.rabbitmq.util.SleepUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消费者:
* 消息在手动应答的时候是不丢失的,放回队列中重新消费
* 模拟手动应答
*/
public class Work03 {
//队列的名称
private final static String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel3 = RabbitMqUtils.getChannel();
System.out.println("手动应答-C3-等待接受消息处理-等待较短时间");
DeliverCallback deliverCallback=(consumerTag, message)->{
//模拟复杂的场景,代码执行1秒后,才可以接受到消息内容,所以执行到这就沉睡1秒钟
SleepUtils.sleep(2);//沉睡2秒
System.out.println("接收到的消息:" + new String(message.getBody(),"utf-8"));
//下面实现手动应答的的一些操作.........
/**
* 1.消息标记 tag,每个消息都有唯一的标识
* 2.是否批量应答未应答消息。false表示不批量应答
*/
channel3.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback=(consumerTag)->{
System.out.println("消息消费被中断");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答,true代表自动应答,false手动应答
* 3.消费者未成功消费的回调
* 4.消费者取消消费的回调
*/
boolean autoAck = false;
channel3.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
b3.消费者2:睡眠30秒后处理消息:
package com.jianqun.rabbitmq.three;
import com.jianqun.rabbitmq.util.RabbitMqUtils;
import com.jianqun.rabbitmq.util.SleepUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消费者:
* 消息在手动应答的时候是不丢失的,放回队列中重新消费
* 模拟手动应答
*/
public class Work04 {
//队列的名称
private final static String QUEUE_NAME = "ack_queue";
//main函数式用来获取生产者发过来的消息
public static void main(String[] args) throws Exception {
Channel channel3 = RabbitMqUtils.getChannel();
System.out.println("手动应答-C4-等待接受消息处理-等待较长时间");
DeliverCallback deliverCallback=(consumerTag, message)->{
//沉睡1秒
SleepUtils.sleep(30);//沉睡30秒
System.out.println("接收到的消息:" + new String(message.getBody(),"utf-8"));
//下面实现手动应答的的一些操作.........
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息。false表示不批量应答
*/
channel3.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback=(consumerTag)->{
System.out.println("消息消费被中断");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答,true代表自动应答,false手动应答
* 3.消费者未成功消费的回调
* 4.消费者取消消费的回调
*/
boolean autoAck = false;
channel3.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
c.测试:分别启动2个消费端和生产者端,然后生产者发送消息测试:
- 1.发送4条消息进行测试:
- 2.如下可以看到达到了预计效果:c1睡眠时间端,处理消息快,c2消费者睡眠时间长,处理消息慢,两个消费者是轮询收到了消息2
5.3.模拟消费者2挂掉:
模拟消费者2在睡眠过程中挂掉了,来看消息如何处理的:
a.下面模拟消费者2挂掉
:
消费者2睡眠时间长,假设它收到生产的消息后,在睡眠过程中挂掉了,消息还没有处理,然后
看看消息队列是否对消息重发
,消费者1能不能收到消息,生产者中输入内容,分别发送AAAAA:
- 1.先是生产端发送消息:
- 2.work03先是收到消息AAAAA:
- 3.生产者再次发送BBBBB
- 4.按照轮询机制,消费者work4应该收到BBBBB,但是消费者2在睡眠过程中突然挂掉了,
那么work04消费者会把BBBB消息会转发给work03,work04会把消息放回队列,然后使消费者work03收到。实际上并不是生产者又进行BBBB消息的重发一遍
注意: work04消费者会把BBBB消息会转发给work03,
work04会把消息放回队列
,然后消费者work03收到。并不是生产者又进行BBBB消息的重发一遍
6.RabbitMQ 持久化:
6.1.什么是RabbitMQ 持久化:
- 1.上述是解决的如何解决理任务不丢失的情况:至少还有一个消费者存活,使消息重新入队,消息重发,消费者再处理就可以了
- 2.在本节中,我们主要解决的是
对列和消息
的持久化,情况是消费者或者生产者都挂掉重启了,如果没有持久化,那就凉了。所以如何保障RabbitMQ 退出或由于某种原因崩溃时。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化
6.2.队列如何实现持久化
-
1.之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果
要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化
-
2.持久化的对列上面有个:“D”
-
3.但是需要注意的就是
如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列
,不然就会出现错误 -
4.管理页面上删除对列的截图:
6.3.消息的持久化:
-
1.
消息实现持久化需在消息生产者修改MessageProperties.PERSISTENT_TEXT_PLAIN
添加如下图所示的这个属性
-
2.将消息标记为持久化并不能完全保证不会丢失消息。
-
3.尽管它告诉 RabbitMQ 将消息保存到磁盘,
但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点
。此时并没有真正写入磁盘。持久性保证并不强。如果需要更强有力的持久化策略,参考后边课件发布确认章节来解决这个问题。