RabbitMQ
3. 简单模式的使用
3.1 Hello Word
在这里,我们将使用java编写两个程序,分别是生产者与消费者,流程为生产者连接MQ发送消息,然后利用消费者去消费这个消息。
-
Maven依赖(java环境)
<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> <!--rabbitmq 依赖客户端--> <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>
-
Producer.java
(生产者代码)package com.lvhaoit.rabbitmq.one; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * Created by IntelliJ IDEA. * User: LvHaoIT (asus) * Date: 2022/5/30 * Time: 9:03 */ public class Producer { public static final String QUEUE_NAME = "hello"; //发消息 public static void main(String[] args) throws Exception { //创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //工厂ip factory.setHost("1.1.1.1"); //用户名 factory.setUsername("lvhao"); //密码 factory.setPassword("*****"); //创建连接 Connection connection = factory.newConnection(); //获取信道 Channel channel = connection.createChannel(); //声明一个序列 /** * 1. 队列名称 * 2. 是否持久化到磁盘 (队列持久化) * 3. 是否共享 * 4. 是否自动删除 * 5. 队列参数 */ channel.queueDeclare(QUEUE_NAME, false, false, false, null); String message = "hello word"; for (int i = 0; i < 10; i++) { //发送消息 /** * 1. 交换机 * 2. 队列名 * 3. 参数 消息持久化 * 4. 消息 */ channel.basicPublish("", QUEUE_NAME, null, (message + i).getBytes()); System.out.println("消息已发送"); } } }
-
Consumer.java
(消费者代码)package com.lvhaoit.rabbitmq.one; import com.rabbitmq.client.*; /** * Created by IntelliJ IDEA. * User: LvHaoIT (asus) * Date: 2022/5/30 * Time: 9:37 */ public class Consumer { public static final String QUEUE_NAME = "hello"; //发消息 public static void main(String[] args) throws Exception { //创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //工厂ip factory.setHost("1.1.5.1"); //用户名 factory.setUsername("lvhao"); //密码 factory.setPassword("******"); //创建连接 Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); // 声明接收消息 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody())); }; //取消消息回调 CancelCallback cancelCallback = consumerTag -> { System.out.println("消息被中断"); }; /** * 1. 队列名称 * 2. 是否自动应答 true自动 * 3. 接收消息处理方法 * 4. 中断消息处理方法 */ channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback); } }
-
操作步骤
-
先运行生产者代码(可以看到生产者成功发送消息)
-
再运行消费者代码(可以看到消费者成功消费生产者发送的消息)
-
运行截图:
-
4. Work Queues
工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。 相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进 程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
4.1 轮询分发消息
这个例子中,我们将启动三个工作线程,一个消息发送线程,看看MQ是如何分发消息的
-
抽取共同代码块,形成可共用工具类。(用来获取信道)
public class RabbitMQUtils { public static Channel getChannel() throws Exception { //创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //工厂ip factory.setHost("121.196.195.124"); //用户名 factory.setUsername("lvhaoit"); //密码 factory.setPassword("lh051920"); //创建连接 Connection connection = factory.newConnection(); //获取信道 Channel channel = connection.createChannel(); return channel; } }
-
编写消费者工作线程
public class Work01 { public static final String QUEUE_NAME = "hello"; // 处理消息 public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtils.getChannel(); // 声明接收消息 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody())); }; //取消消息回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消息被中断"); }; System.out.println("C3正在的等待接收消息"); channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback); } }
-
编写生产者生产线程
/** * 消息处理轮询机制,一个消费线程消费一个 交错消费 */ public class Task01 { public static final String QUEUE_NAME = "hello"; //发消息 public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtils.getChannel(); 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); } } }
-
操作步骤
-
打开消费者的并行选项
-
同时启动三个消费者线程
-
打开生产者线程,并发送三条消息
-
观察启动的三个消费者进程,可以发现如下情况
-
-
得出结论工作队列默认是以轮询的方式进行消息分配,即人人平等,本着公平原则,一人一条,交错消费。
4.2 消息应答
4.2.1 概念
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成 了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消 息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续 发送给该消费这的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接 收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
4.2.2 自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并 以某种速率能够处理这些消息的情况下使用
通俗来说:当使用自动应答的时候能够更加的高效,但是对工作环境有着更高的要求,万一其中发生MQ服务器宕机或者消息过度积压,就有可能造成消息丢失的情况。
4.2.3 消息应答的方法(手动应答)
-
Channel.basicAck(用于肯定确认)
使用场景:RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
-
Channel.basicNack(用于否定确认)
-
Channel.basicReject(用于否定确认)
与 Channel.basicNack 相比少一个参数
使用场景:不处理该消息了直接拒绝,可以将其丢弃了
优点:手动应答的好处就是可以批量应答并且减少网络拥堵。
4.2.4 Multiple的解释
Channel.basicAck(deliveryTag,true)
这里的true就是multiple,意思是代表批量应答
批量应答:假设channel(信道)上传送tag的消息有1,2,3,4,当前tag是4,那么此时1-4这些还未应答的消息都会被批量收到消息应答。
当这里为false时,每次只会应答当前此条消息的应答。
为了保证队列中的消息不出现消息丢失情况,最好将此选项设置为false。
4.2.5 消息的自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息 未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者 可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确 保不会丢失任何消息。
4.2.6 手动应答效果
-
消费线程 C3 (该线程会在处理消息时,线程沉睡三十秒,模拟处理消息慢的工作线程)
public class Work03 { public static final String QUEUE_NAME = "ack_queue"; // 处理消息 public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtils.getChannel(); // 声明接收消息 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody(), "UTF-8")); try { //睡眠三十秒 Thread.sleep(1000 * 30); } catch (InterruptedException e) { e.printStackTrace(); } //手动应答 /** * 1.消息的标记 * 2.是否需要批量应答 最好不要批量应答,批量应答有可能造成消息丢失 */ channel.basicAck(message.getEnvelope().getDeliveryTag(), false); System.out.println("处理结束!"); }; //取消消息回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消息被中断"); }; System.out.println("C3(执行时间较长)正在的等待接收消息"); //采用手动应答 boolean autoACK = false; /** * 1. 队列名称 * 2. 是否自动应答 true自动 * 3. 接收消息处理方法 * 4. 中断消息处理方法 */ channel.basicConsume(QUEUE_NAME, autoACK, deliverCallback, cancelCallback); } }
-
消费线程 C4 (该线程会在处理消息时,线程沉睡两秒,模拟处理消息快的工作线程)
public class Work04 { public static final String QUEUE_NAME = "ack_queue"; // 处理消息 public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtils.getChannel(); // 声明接收消息 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody(), "UTF-8")); try { //睡眠2秒 Thread.sleep(1000 * 2); } catch (InterruptedException e) { e.printStackTrace(); } //手动应答 /** * 1.消息的标记 * 2.是否需要批量应答 最好不要批量应答,批量应答有可能造成消息丢失 */ channel.basicAck(message.getEnvelope().getDeliveryTag(), false); System.out.println("处理结束!"); }; //取消消息回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消息被中断"); }; System.out.println("C4(执行时间较短)正在的等待接收消息"); //采用手动应答 boolean autoACK = false; /** * 1. 队列名称 * 2. 是否自动应答 true自动 * 3. 接收消息处理方法 * 4. 中断消息处理方法 */ channel.basicConsume(QUEUE_NAME, autoACK, deliverCallback, cancelCallback); } }
-
生产线程
/** * 生产者 * 让其中一个消费线程中断宕机,消费的信息会重新丢回队列给新的消费者重新消费 */ public class Task02 { public static final String QUEUE_NAME = "ack_queue"; //发消息 public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtils.getChannel(); /** * 1。 队列名称 * 2. 是否持久化到磁盘 (队列持久化) * 3. 是否共享 * 4. 是否自动删除 * 5. 队列参数 */ channel.queueDeclare(QUEUE_NAME, true, false, false, null); //循环发消息 Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String message = scanner.next(); /** * 1. 交换机 * 2. 队列名 * 3. 参数 消息持久化 * 4. 消息 */ channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8")); System.out.println("生产者发出消息: " + message); } } }
-
操作步骤
-
打开两个消费进程与一个生产进程
-
用生产进程发送四个消息
-
可以发现c3进程第一个消息还没处理完成,此时c4进程两条消息已经都处理完成,c3进程处理中一条消息,堆积了一条消息,此时关闭c3进程,可知c3进程处理中的消息也未能正确进行消息应答。
-
此时发现c4进行将c3未处理完的消息和堆积的消息一起拿过来处理掉了,可以证明c3中的消息重新入列了。
-