3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列
@
目次
1. RabbitMQ 的经典国际代码(Hello World)
我们将用 Java 编写两个程序。发送单个消息的生产者和吸收消息并打印 出来的消费者。我们将先容 Java API 中的一些细节。
在下图中,“ P”是我们的生产者,“ C”是我们的消费者。中间的框是一个队列-RabbitMQ 代 表使用者保留的消息缓冲区
创建一个 rabbitmq-hello Module 模块,导入 RabbitMQ 的相关依赖在 pom.xml 文件中。添加跋文得刷新一下 Maven
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <source>8</source>
- <target>8</target>
- </configuration>
- </plugin>
- </plugins>
- </build>
- <dependencies>
-
- <dependency>
- <groupId>com.rabbitmq</groupId>
- <artifactId>amqp-client</artifactId>
- <version>5.8.0</version>
- </dependency>
-
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>2.6</version>
- </dependency>
- </dependencies>
复制代码 创建一个生产者发送消息:
 - package com.rainbowsea.rabbitmq.one;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.Connection;
- import com.rabbitmq.client.ConnectionFactory;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- /**
- * 生产者,发送消息
- */
- public class Producer {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "hello";
- // 生产者,发送消息
- public static void main(String[] args) throws IOException, TimeoutException {
- // 创建一个连接工厂
- ConnectionFactory connectionFactory = new ConnectionFactory();
- // 工厂IP连接 RabbitMQ的队列
- connectionFactory.setHost("192.168.76.156"); // Rabbitmq 所在的IP地址
- // 连接 RabbitMQ 的用户名
- connectionFactory.setUsername("admin");
- // 连接 RabbitMQ 的密码
- connectionFactory.setPassword("123");
- // 创建连接
- Connection connection = connectionFactory.newConnection();
- // 获取信道
- Channel channel = connection.createChannel();
- /*
- 生成一个队列
- 1.队列名称
- 2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
- 3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
- 4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
- 5.其它参数
- */
- channel.queueDeclare(QUEUE_NAME,false,false,false,null);
- // 发消息
- String message = "Hello World"; // 初次发送消息
- /*
- 发送一个消费
- 1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
- 2.路由的Key值是哪个,本次是队列的名字
- 3. 其它参数信息
- 4. 发送消息的消息体,要转换为 bit 流进行处理发送
- */
- channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
- System.out.println("消息发送完毕");
- }
- }
复制代码
我们打开浏览器:检察 RabbitMQ 的图形化管理页面:http://192.168.76.156:15672/#/queues



创建一个 Consumer 类作为消费者——> 读取/消费,该名为 hello 队列当中的消息信息。
 - package com.rainbowsea.rabbitmq.one;
- import com.rabbitmq.client.CancelCallback;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.Connection;
- import com.rabbitmq.client.ConnectionFactory;
- import com.rabbitmq.client.DeliverCallback;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- /**
- * 消费者读取消息,
- * 消费消息
- */
- public class Consumer {
- // 队列名称
- public static final String QUEUE_NAME = "hello";
- // 生产者,发送消息
- public static void main(String[] args) throws IOException, TimeoutException {
- // 创建一个连接工厂
- ConnectionFactory connectionFactory = new ConnectionFactory();
- // 工厂IP连接 RabbitMQ的队列
- connectionFactory.setHost("192.168.76.156"); // Rabbitmq 所在的IP地址
- // 连接 RabbitMQ 的用户名
- connectionFactory.setUsername("admin");
- // 连接 RabbitMQ 的密码
- connectionFactory.setPassword("123");
- // 创建连接
- Connection connection = connectionFactory.newConnection();
- // 获取信道
- Channel channel = connection.createChannel();
- // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- DeliverCallback deliverCallback = (consumerTag, message) -> {
- System.out.println("读取到的消息" + message);
- String messages = new String(message.getBody());
- System.out.println("读取到的消息" + messages);
- };
- // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- CancelCallback cancelCallback = (consumerTag) -> {
- System.out.println("没有读取到的消息,消息在读取过程中中断了");
- };
- /*
- 消费者消费/读取消息
- 1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
- 2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
- 3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- 4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- */
- channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
- }
- }
复制代码
2. RabbitMQ 的 Work Queues(工作队列)
Work Queues(工作队列又称任务队列): 的主要思想是避免立即实行资源密集型任务,而不得不等待它完成。相反我们安排任务之后实行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终实行作业。当有多个工作线程时,这些工作线程将一起处置惩罚这些任务。
2.1 RabbitMQ 的 轮训接受/消费消息
在这个案例中我们会启动两个工作线程,一个消息发送线程,我们来看看他们两个工作线程 是如何工作的。
这里我们将一个获取频道的工具抽取出来作为一个工具类使用。
- package com.rainbowsea.rabbitmq.utils;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.Connection;
- import com.rabbitmq.client.ConnectionFactory;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- /**
- * 此类为连接工厂创建信道的工具类
- */
- public class RabbitMQUtils {
- /**
- * 得到一个连接的 channel
- * @return
- * @throws IOException
- * @throws TimeoutException
- */
- public static Channel getChannel() throws IOException, TimeoutException {
- // 创建一个连接工厂
- ConnectionFactory connectionFactory = new ConnectionFactory();
- // 工厂IP连接 RabbitMQ的队列
- connectionFactory.setHost("192.168.76.156"); // Rabbitmq 所在的IP地址
- // 连接 RabbitMQ 的用户名
- connectionFactory.setUsername("admin");
- // 连接 RabbitMQ 的密码
- connectionFactory.setPassword("123");
- // 创建连接
- Connection connection = connectionFactory.newConnection();
- // 获取信道
- Channel channel = connection.createChannel();
- return channel;
- }
- }
复制代码 启动一个发送线程(作为生产者,发送消息)
 - package com.rainbowsea.rabbitmq.two;
- import com.rabbitmq.client.Channel;
- import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
- import java.io.IOException;
- import java.util.Scanner;
- import java.util.concurrent.TimeoutException;
- /**
- * 生产者发送消息
- */
- public class Task01 {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "hello";
- /**
- * 发送大量消息
- *
- * @param args
- */
- public static void main(String[] args) throws IOException, TimeoutException {
- Channel channel = RabbitMQUtils.getChannel();
- /*
- 生成一个队列
- 1.队列名称
- 2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
- 3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
- 4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
- 5.其它参数
- */
- channel.queueDeclare(QUEUE_NAME, false, false, false, null);
- // 从控制台当中接受消息
- Scanner scanner = new Scanner(System.in);
- while (scanner.hasNext()) {
- String message = scanner.next();
- /*
- 发送一个消费
- 1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
- 2.路由的Key值是哪个,本次是队列的名字
- 3. 其它参数信息
- 4. 发送消息的消息体,要转换为 bit 流进行处理发送
- */
- channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
- System.out.println("发送消息完成:" + message);
- }
- }
- }
复制代码 启动两个工作线程(作为消费者,读取/消费信息)
 - package com.rainbowsea.rabbitmq.two;
- import com.rabbitmq.client.CancelCallback;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.DeliverCallback;
- import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- public class Worker01 {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "hello";
- public static void main(String[] args) throws IOException, TimeoutException {
- Channel channel = RabbitMQUtils.getChannel();
- // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- DeliverCallback deliverCallback = (consumerTag, message) -> {
- System.out.println("读取到的消息" + new String(message.getBody()));
- };
- // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- CancelCallback cancelCallback = (consumerTag) -> {
- System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
- };
- /*
- 消费者消费/读取消息
- 1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
- 2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
- 3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- 4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- */
- System.out.println("C1 等待接收消息");
- channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
- }
- }
复制代码 - package com.rainbowsea.rabbitmq.two;
- import com.rabbitmq.client.CancelCallback;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.DeliverCallback;
- import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- public class Worker02 {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "hello";
- public static void main(String[] args) throws IOException, TimeoutException {
- Channel channel = RabbitMQUtils.getChannel();
- // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- DeliverCallback deliverCallback = (consumerTag, message) -> {
- System.out.println("读取到的消息" + new String(message.getBody()));
- };
- // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- CancelCallback cancelCallback = (consumerTag) -> {
- System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
- };
- /*
- 消费者消费/读取消息
- 1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
- 2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
- 3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- 4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- */
- System.out.println("C2 等待接收消息");
- channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
- }
- }
复制代码 运行实行效果:
通过程序实行发现生产者总共发送 4 个消息,消费者 1 和消费者 2 分别分得两个消息,并且 是按照有序的一个吸收一次消息。
2.2 RabbitMQ 的 消息应答
消费者完成一个任务可能需要一段时间,假如此中一个消费者处置惩罚一个长的任务并仅只完成了部门任务,就忽然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标志为删除。在这种情况下,忽然有个消费者挂掉了,我们将丢失正在处置惩罚的消息。以及后续发送给该消费这的消息,因为它无法吸收到。
为了保证消息在发送过程中不丢失,RabbitMQ 引入消息应答机制,消息应答就是:消费者在吸收到消息并且处置惩罚该消息之后,告诉 RabbitMQ 它已经处置惩罚了,RabbitMQ 可以把该消息删除了 。
自动应答:
消息发送后立即被认为已经传送乐成,这种模式需要在高吞吐量和数据传输安全性方面做衡量,因为这种模式假如消息在吸收到之前,消费者那里出现毗连或者 channel 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那里可以传递过载的消息,没有对传递的消息数量举行限制, 当然这样有可能使得消费者这边由于吸收太多还来不及处置惩罚的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操纵系统杀死,所以这种模式仅适用在消费者可以高效并 以某种速率能够处置惩罚这些消息的情况下使用。
消息应答的方法:- A.Channel.basicAck(用于肯定确认)
- RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
- B.Channel.basicNack(用于否定确认)
- C.Channel.basicReject(用于否定确认)
- 与 Channel.basicNack 相比少一个参数
- 不处理该消息了直接拒绝,可以将其丢弃了
复制代码 Multiple 的解释:
手动应答的好处是可以批量应答并且减少网络拥堵。
multiple 的 true 和 false 代表不同意思
true 代表批量应答 channel 上未应答的消息
比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答
false 同上面相比
只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
消息自动重新入队:
假如消费者由于某些缘故原由失去毗连(其通道已关闭,毗连已关闭或 TCP 毗连丢失),导致消息 未发送 ACK 确认,RabbitMQ 将了解到消息未完全处置惩罚,并将对其重新排队。假如此时其他消费者 可以处置惩罚,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔殒命,也可以确 保不会丢失任何消息。
消息手动应答代码:
认消息接纳的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改 为手动应答,消费者在上面代码的根本上增加下面画红色部门代码。
- // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- DeliverCallback deliverCallback = (consumerTag, message) -> {
- // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
- System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));
- /*
- 手动应答
- 1. 消息的标记 tag
- 2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
- */
- channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
- };
- /*
- 消费者消费/读取消息
- 1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
- 2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
- 3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- 4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- */
- System.out.println("C1 等待接收消息");
- boolean autoAck = false; // 采用手动应答的方式
- channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
复制代码留意:手动应答的编写是写在,消费者/读取者的,读取乐成的 DeliverCallback 回调函数当中的,此中的,后面的 channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback); 第二个参数要改为 false 表示,启动手动应答的方式 。
这里我们额外编写一个,睡眠的工具类,用于模仿消费者读取/消费RabbitMQ 消息队列当中的信息的一个网络延时。如下:
- package com.rainbowsea.rabbitmq.utils;
- /**
- * 睡眠工具类
- */
- public class SleepUtils {
- public static void sleep(int second) {
- try {
- Thread.sleep(1000 * second); // 单位毫秒
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- }
复制代码 编写两个消费者/读取 RabbitMQ 消息队列当中的消息。一个消费者,模拟延时 1s ,别的一个消费者模拟延时30s 。
一个消费者,模拟延时 1 S
 - package com.rainbowsea.rabbitmq.three;
- import com.rabbitmq.client.CancelCallback;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.DeliverCallback;
- import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
- import com.rainbowsea.rabbitmq.utils.SleepUtils;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- public class Worker03 {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "ack_queue";
- public static void main(String[] args) throws IOException, TimeoutException {
- Channel channel = RabbitMQUtils.getChannel();
- System.out.println("C1 等待接收消息处理时间较短");
- // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- CancelCallback cancelCallback = (consumerTag) -> {
- System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
- };
- // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- DeliverCallback deliverCallback = (consumerTag, message) -> {
- // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
- System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));
- // 读取时,睡眠 1 s
- SleepUtils.sleep(1);
- /*
- 手动应答
- 1. 消息的标记 tag
- 2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
- */
- channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
- };
- /*
- 消费者消费/读取消息
- 1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
- 2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
- 3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- 4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- */
- boolean autoAck = false; // 采用手动应答的方式
- channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
- }
- }
复制代码 一个消费者,模拟延时 30 S
 - package com.rainbowsea.rabbitmq.three;
- import com.rabbitmq.client.CancelCallback;
- import com.rabbitmq.client.Channel;
- import com.rabbitmq.client.DeliverCallback;
- import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
- import com.rainbowsea.rabbitmq.utils.SleepUtils;
- import java.io.IOException;
- import java.util.concurrent.TimeoutException;
- public class Worker04 {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "ack_queue";
- public static void main(String[] args) throws IOException, TimeoutException {
- Channel channel = RabbitMQUtils.getChannel();
- System.out.println("C2 等待接收消息处理时间较长");
- // 消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- CancelCallback cancelCallback = (consumerTag) -> {
- System.out.println(consumerTag + "消费者取消消费接口执行回调逻辑");
- };
- // 声明: 消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- DeliverCallback deliverCallback = (consumerTag, message) -> {
- // new String(message.getBody(),"UTF-8" 如果这里接收的消息内容是中文的,需要将其转换为 utf-8的内容
- System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8"));
- // 读取时,睡眠 30 s
- SleepUtils.sleep(30);
- /*
- 手动应答
- 1. 消息的标记 tag
- 2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量
- */
- channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
- };
- /*
- 消费者消费/读取消息
- 1.消费/读取哪个队列当中的消息(注意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了)
- 2.消费成功之后是否要自动应答 true 代表的自动应答,false 代表手动应答
- 3.消费者未成功消费/读取到队列当中的信息后,的执行的回调函数
- 4.消费者成功消费/读取到队列当中的信息后,的执行的回调函数
- */
- boolean autoAck = false; // 采用手动应答的方式
- channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
- }
- }
复制代码 生产者,发送消息
手动应答,生产者,不需要做出什么修改上的操纵,就是发送消息,让消费者读取/消费
 - package com.rainbowsea.rabbitmq.three;
- import com.rabbitmq.client.Channel;
- import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;
- import java.io.IOException;
- import java.util.Scanner;
- import java.util.concurrent.TimeoutException;
- /**
- * 消息在手动改应答时是不丢失,返回队列当中重新应答
- */
- public class Task2 {
- // ctrl + shift + u 大写转换
- // 队列名称
- public static final String QUEUE_NAME = "ack_queue";
- /**
- * 发送大量消息
- *
- * @param args
- */
- public static void main(String[] args) throws IOException, TimeoutException {
- Channel channel = RabbitMQUtils.getChannel();
- /*
- 生成一个队列
- 1.队列名称
- 2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
- 3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
- 4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
- 5.其它参数
- */
- channel.queueDeclare(QUEUE_NAME, false, false, false, null);
- // 从控制台当中接受消息
- Scanner scanner = new Scanner(System.in);
- while (scanner.hasNext()) {
- String message = scanner.next();
- /*
- 发送一个消费
- 1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
- 2.路由的Key值是哪个,本次是队列的名字
- 3. 其它参数信息
- 4. 发送消息的消息体,要转换为 bit 流进行处理发送
- */
- channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
- System.out.println("发送消息完成:" + message);
- }
- }
- }
复制代码 运行实行结果:
正常情况下消息发送方发送两个消息 C1 和 C2 分别吸收到消息并举行处置惩罚
这里:我们的发送者发送消息 dd,发出消息之后的时间,我们就把(C2 消费者处置惩罚时间 30 S 比较长的停止) ,按照 RabbitMQ 默认的轮询读取的操纵,这时间的 dd 应该是由 C2 消费者/读取处置惩罚该消息的,但是由于这个时间 C2 30s 的延时,并没有将 dd 消息处置惩罚掉,就被我们停止了,此时,我们发现的结果运行结果就是,该消息 dd 被 C1 吸收到了,这就说明:手动应答,因为 C2 处置惩罚 dd消息的时间被中途停止了,并没有将该 dd消息处置惩罚掉,全部没有向 RabbitMQ 消息队列作出一个应答的操纵,那么该消息就没有被 RabbitMQ 删撤除,而是把 dd 这个消息交给分配给了 C1 消费者处置惩罚了。
特别说明:
- 这里我们接纳的时 手动应答 的方式,它是在一个RabbitMQ 消息队列的一个默认的轮询读取 的方式下的。
- 手动应答: 实现的效果就是:对应某个消费者,因为某种缘故原由(网络延时),没有实时将,它该在轮询读取的情况下,并没有将全部的消息给读取完,因为没有读取完,又因为接纳的是一个 手动应答的方式,队列知道它没有将,该队列当中的消息读取完,全部 RabbitMQ 消息队列,不会将该消息删除,而是等到该消费者 将该队列的内容读取完才会,删除该队列当中的内容。
- 特别留意:我们这里的两个消费者,假如一开始在,RabbitMQ 没有声明对应的 channel信道,队列信息,那么需要先启动生产者,否则消费者,找不到对应,队列信息,是会报错,无法运行的。
2.3 RabbitMQ 的 持久化
概念:
刚刚我们已经看到了如何处置惩罚任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉之后,消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种缘故原由崩溃时,它忽视队列和消息。除非告知它不要这样做,确保消息不会丢失需要两件事:我们需要将队列和消息都标志为持久化 。
2.3.1 RabbitMQ 当中队列的持久化:
之前我们创建的队列都是非持久化的,rabbitmq 假如重启的化,该队列就会被删撤除,假如 要队列实现持久化 需要在声明队列的时间把 durable 参数设置为持久化
- /*
- 生成一个队列
- 1.队列名称
- 2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
- 3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
- 4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
- 5.其它参数
- */
- boolean durable = true;
- channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
复制代码 但是需要留意的就是假如之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,否则就会出现错误。缘故原由:就是无法将一个一开始不是持久化的队列修改为持久化的队列 。
我们需要将该队列删除,重新天生一个队列,让其一开始就是一个持久化的队列。
补充:
以下为控制台中持久化与非持久化队列的 UI 表现区:
这个时间即使重启 rabbitmq 队列也依然存在
留意:上面我们仅仅只是将 RabbitMQ 当中的队列举行了持久化,但是此中 RabbitMQ 当中的消息是没有被持久化的。
2.3.2 RabbitMQ 消息的持久化:
要想让消息实现持久化需要在生产者 发送者,修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添 加这个属性。
- /*
- 发送一个消费
- 1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
- 2.路由的Key值是哪个,本次是队列的名字
- 3. 其它参数信息
- 4. 发送消息的消息体,要转换为 bit 流进行处理发送
- */
- channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
- System.out.println("发送消息完成:" + message);
复制代码 将消息标志为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是 这里依然存在当消息刚准备存储在磁盘的时间 但是还没有存储完,消息还在缓存的一个间隔点。此时并没 有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。假如需要 更强有力的持久化策略,参考该博客的后续章节。
2.4 RabbitMQ 的 不公平分发(简单的说类似于权重分发)
在最开始的时间我们学习到 RabbitMQ 分发消息接纳的轮训分发,但是在某种场景下这种策略并不是 很好,比方说有两个消费者在处置惩罚任务,此中有个消费者 1 处置惩罚任务的速度非常快,而别的一个消费者 2 处置惩罚速度却很慢,这个时间我们还是接纳轮训分发的化就会到这处置惩罚速度快的这个消费者很大一部门时间 处于空闲状态,而处置惩罚慢的那个消费者不停在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的举行分发。
为了避免这种情况,我们可以设置参数 channel.basicQos(1);
- // 设置不公平分发
- int prefetchCount = 1;
- channel.basicQos(prefetchCount);
复制代码补充:
意思就是假如这个任务我还没有处置惩罚完或者我还没有应答你,你先别分配给我,我如今只能处置惩罚一个 任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然假如全部的消费者都没有完 成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时间就只能添加 新的 worker 或者改变其他存储任务的策略。
运行测试:连续使用上面我们 RabbitMQ 手动应答的代码,仅仅只修改此中的消费者的channel.basicQos(prefetchCount); 参数 ,生产者不需要修改。
 - package com.rainbowsea.rabbitmq.three;import com.rabbitmq.client.CancelCallback;import com.rabbitmq.client.Channel;import com.rabbitmq.client.DeliverCallback;import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;import com.rainbowsea.rabbitmq.utils.SleepUtils;import java.io.IOException;import java.util.concurrent.TimeoutException;public class Worker03 { // ctrl + shift + u 大写转换 // 队列名称 public static final String QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); System.out.println("C1 等待吸收消息处置惩罚时间较短"); // 消费者未乐成消费/读取到队列当中的信息后,的实行的回调函数 CancelCallback cancelCallback = (consumerTag) -> { System.out.println(consumerTag + "消费者取消消费接口实行回调逻辑"); }; // 声明: 消费者乐成消费/读取到队列当中的信息后,的实行的回调函数 DeliverCallback deliverCallback = (consumerTag, message) -> { // new String(message.getBody(),"UTF-8" 假如这里吸收的消息内容是中文的,需要将其转换为 utf-8的内容 System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8")); // 读取时,睡眠 1 s SleepUtils.sleep(1); /* 手动应答 1. 消息的标志 tag 2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }; /* 消费者消费/读取消息 1.消费/读取哪个队列当中的消息(留意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了) 2.消费乐成之后是否要自动应答 true 代表的自动应答,false 代表手动应答 3.消费者未乐成消费/读取到队列当中的信息后,的实行的回调函数 4.消费者乐成消费/读取到队列当中的信息后,的实行的回调函数 */ // 设置不公平分发
- int prefetchCount = 1;
- channel.basicQos(prefetchCount); boolean autoAck = false; // 接纳手动应答的方式 channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback); }}
复制代码- package com.rainbowsea.rabbitmq.three;import com.rabbitmq.client.Channel;import com.rabbitmq.client.MessageProperties;import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;import java.io.IOException;import java.util.Scanner;import java.util.concurrent.TimeoutException;/** * 消息在手动改应答时是不丢失,返回队列当中重新应答 */public class Task2 { // ctrl + shift + u 大写转换 // 队列名称 public static final String QUEUE_NAME = "ack_queue"; /** * 发送大量消息 * * @param args */ public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); /*
- 生成一个队列
- 1.队列名称
- 2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
- 3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
- 4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
- 5.其它参数
- */
- boolean durable = true;
- channel.queueDeclare(QUEUE_NAME, durable, false, false, null); // 从控制台当中接受消息 Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String message = scanner.next(); /*
- 发送一个消费
- 1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
- 2.路由的Key值是哪个,本次是队列的名字
- 3. 其它参数信息
- 4. 发送消息的消息体,要转换为 bit 流进行处理发送
- */
- channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
- System.out.println("发送消息完成:" + message); } }}
复制代码

运行结果:
2.5 RabbitMQ 当中的“预取值” 设置
本身消息的发送就是异步发送的,所以在任何时间,channel 上肯定不止只有一个消息别的来自消费 者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此 缓冲区的大小,以避免缓冲区内里无限制的未确认消息问题。这个时间就可以通过使用 basic.qos 方法设 置“预取计数”值来完成的。该值界说通道上允许的未确认消息的最大数量。一旦数量达到配置的数量, RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处置惩罚的消息被确认,例如,假设在通道上有 未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何 消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知 这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有庞大影响。通常,增加预取将提高 向消费者传递消息的速度。固然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处置惩罚的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处置惩罚 的自动确认模式或手动确认模式,消费者消费了大量的消息假如没有确认的话,会导致消费者毗连节点的 内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这 将使吞吐量变得很低,特别是消费者毗连耽误很严峻的情况下,特别是在消费者毗连等待时间较长的环境 中。对于大多数应用来说,稍微高一点的值将是最佳的。
**该阈值设置和,不公平分发设置是一样的,编码是一样的,不同的是该设置的值,不是 1了,而是具体的一个该消费者具体要/能处置惩罚的”阈值“了 **
- // 设置预取值为: 2
- int prefetchCount = 2;
- channel.basicQos(prefetchCount);
复制代码
- // 设置预取值为: 5
- int prefetchCount = 5;
- channel.basicQos(prefetchCount);
复制代码- package com.rainbowsea.rabbitmq.three;import com.rabbitmq.client.CancelCallback;import com.rabbitmq.client.Channel;import com.rabbitmq.client.DeliverCallback;import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;import com.rainbowsea.rabbitmq.utils.SleepUtils;import java.io.IOException;import java.util.concurrent.TimeoutException;public class Worker04 { // ctrl + shift + u 大写转换 // 队列名称 public static final String QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); System.out.println("C2 等待吸收消息处置惩罚时间较长"); // 消费者未乐成消费/读取到队列当中的信息后,的实行的回调函数 CancelCallback cancelCallback = (consumerTag) -> { System.out.println(consumerTag + "消费者取消消费接口实行回调逻辑"); }; // 声明: 消费者乐成消费/读取到队列当中的信息后,的实行的回调函数 DeliverCallback deliverCallback = (consumerTag, message) -> { // new String(message.getBody(),"UTF-8" 假如这里吸收的消息内容是中文的,需要将其转换为 utf-8的内容 System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8")); // 读取时,睡眠 30 s SleepUtils.sleep(30); /* 手动应答 1. 消息的标志 tag 2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }; /* 消费者消费/读取消息 1.消费/读取哪个队列当中的消息(留意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了) 2.消费乐成之后是否要自动应答 true 代表的自动应答,false 代表手动应答 3.消费者未乐成消费/读取到队列当中的信息后,的实行的回调函数 4.消费者乐成消费/读取到队列当中的信息后,的实行的回调函数 */ // 设置不公平分发 //int prefetchCount = 1; // 设置预取值为: 5
- int prefetchCount = 5;
- channel.basicQos(prefetchCount); boolean autoAck = false; // 接纳手动应答的方式 channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback); }}
复制代码 运行测试:连续使用上面我们 RabbitMQ 手动应答的代码,仅仅只修改此中的消费者的int prefetchCount = 5; channel.basicQos(prefetchCount); 参数 ,生产者不需要修改。- package com.rainbowsea.rabbitmq.three;import com.rabbitmq.client.Channel;import com.rabbitmq.client.MessageProperties;import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;import java.io.IOException;import java.util.Scanner;import java.util.concurrent.TimeoutException;/** * 消息在手动改应答时是不丢失,返回队列当中重新应答 */public class Task2 { // ctrl + shift + u 大写转换 // 队列名称 public static final String QUEUE_NAME = "ack_queue"; /** * 发送大量消息 * * @param args */ public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); /*
- 生成一个队列
- 1.队列名称
- 2. 队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中,false 表示不持久化
- 3. 该队列是否只为一个消费者进行消费,是否进行消息共享,true 可以多个消费者消费;false 只能一个消费者消费
- 4. 是否自动删除,最后一个消费者端打开连接以后,该队消息是否删除,true自动删除,false 不自动删除
- 5.其它参数
- */
- boolean durable = true;
- channel.queueDeclare(QUEUE_NAME, durable, false, false, null); // 从控制台当中接受消息 Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String message = scanner.next(); /*
- 发送一个消费
- 1.发送到哪个交换机当中,本次空着,使用RabbitMQ默认交换机
- 2.路由的Key值是哪个,本次是队列的名字
- 3. 其它参数信息
- 4. 发送消息的消息体,要转换为 bit 流进行处理发送
- */
- channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
- System.out.println("发送消息完成:" + message); } }}
复制代码- package com.rainbowsea.rabbitmq.three;import com.rabbitmq.client.CancelCallback;import com.rabbitmq.client.Channel;import com.rabbitmq.client.DeliverCallback;import com.rainbowsea.rabbitmq.utils.RabbitMQUtils;import com.rainbowsea.rabbitmq.utils.SleepUtils;import java.io.IOException;import java.util.concurrent.TimeoutException;public class Worker03 { // ctrl + shift + u 大写转换 // 队列名称 public static final String QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws IOException, TimeoutException { Channel channel = RabbitMQUtils.getChannel(); System.out.println("C1 等待吸收消息处置惩罚时间较短"); // 消费者未乐成消费/读取到队列当中的信息后,的实行的回调函数 CancelCallback cancelCallback = (consumerTag) -> { System.out.println(consumerTag + "消费者取消消费接口实行回调逻辑"); }; // 声明: 消费者乐成消费/读取到队列当中的信息后,的实行的回调函数 DeliverCallback deliverCallback = (consumerTag, message) -> { // new String(message.getBody(),"UTF-8" 假如这里吸收的消息内容是中文的,需要将其转换为 utf-8的内容 System.out.println("读取到的消息" + new String(message.getBody(),"UTF-8")); // 读取时,睡眠 1 s SleepUtils.sleep(1); /* 手动应答 1. 消息的标志 tag 2. 是否批量应答 false : 不批量应答信道中的消息 true 批量,false 不批量 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }; /* 消费者消费/读取消息 1.消费/读取哪个队列当中的消息(留意:一旦读取到了该队列中的某条消息,该消息就被消费者消费掉了,就从队列当中删除了) 2.消费乐成之后是否要自动应答 true 代表的自动应答,false 代表手动应答 3.消费者未乐成消费/读取到队列当中的信息后,的实行的回调函数 4.消费者乐成消费/读取到队列当中的信息后,的实行的回调函数 */ // 设置不公平分发 //int prefetchCount = 1; // 设置预取值为: 2
- int prefetchCount = 2;
- channel.basicQos(prefetchCount); boolean autoAck = false; // 接纳手动应答的方式 channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback); }}
复制代码
运行结果:
我们可以看到
固然我们这里C1 设置了预取值:是读取2 个,但是这里它却读取到了4个
,是因为,它的预取值的计算是从,它是实时更新的:比如一开始处置惩罚了2个
,那么就是0个,那它就是可以再处置惩罚 2个消息,主要是这个 C1 处置惩罚得太快了
固然 C2 设置的是处置惩罚 5 个但是处置惩罚的太慢了
,由于 C1 处置惩罚的太快了,将C2 的处置惩罚的消息
抢走了,所以预取值,不是绝对的,而是相对的。
3. 最后:
“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |