04-rabbitmq-工作队列-spring
来源:互联网 发布:阿里云虚拟主机购买 编辑:程序博客网 时间:2024/06/18 18:15
04-rabbitmq-工作队列-spring
【博文总目录>>>】
【工程下载>>>】
先决条件
本教程假定RabbitMQ已在标准端口(5672)上的localhost上安装并运行。如果使用不同的主机,端口或凭据,连接设置将需要调整。
工作队列(使用spring-ampq客户端)
在第一个教程中,我们编写了程序来发送和接收来自命名队列的消息。在这一个中,我们将创建一个工作队列,用于在多个工作人员之间分配耗时的任务。
工作队列(又称:任务队列)背后的主要思想是避免立即执行资源密集型,并且必须等待完成的任务。相反,我们安排任务在后续完成。我们将任务封装成 消息,并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当你运行很多工作进程时,这些任务将在它们之间共享。
这个概念在Web应用程序中特别有用,尤其适用在短时间HTTP请求窗口中无法处理复杂的任务。
准备
在本教程的前面部分,我们发送了一个包含“Hello World!”的消息。现在我们将发送的的字符串代表复杂任务。我们没有一个现实世界的任务,比如图像被调整大小,或者是要渲染的pdf文件来模拟,所以假设我们很忙 - 通过使用Thread.sleep()函数来假冒它。我们将把字符串中的点数作为其复杂度; 每个点都将占“工作”的一秒钟。例如,由Hello …描述的假任务将需要三秒钟。
如果您尚未设置项目,请参阅第一个教程中的设置。我们将遵循与第一个教程中相同的模式:1)创建一个包(com.example.rabbitmq)并创建一个Tut2Config,Tut2Receiver和Tut2Sender。首先创建一个新包(com.example.rabbitmq),我们将放置我们的三个类。在配置类中,我们设置两个配置文件,教程的标签(“tut2”)和模式的名称(“work-queues”)。我们利用spring来将队列暴露为一个bean。我们将接收者设置为配置文件,并定义两个bean,以对应于上图中的工作进程; receiver1和receiver2。最后,我们定义发件者的配置文件并定义发件者bean。配置现在完成了。
package com.example.rabbitmq;import org.springframework.amqp.core.Queue;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Profile;/** * RabbitMQ消息配置 * Author: 王俊超 * Date: 2017-06-17 10:26 * All Rights Reserved !!! */@Profile({"tut2", "work-queues"})@Configurationpublic class Tut2Config { /** * 创建一个消息队列 * @return */ @Bean public Queue hello() { return new Queue("hello"); } /** * 定一个接收配置对象,定义两个接收者 */ @Profile("receiver") private static class ReceiverConfig { @Bean public Tut2Receiver receiver1() { return new Tut2Receiver(1); } @Bean public Tut2Receiver receiver2() { return new Tut2Receiver(2); } } /** * 定义一个消息发送对象 * * @return */ @Profile("sender") @Bean public Tut2Sender sender() { return new Tut2Sender(); }}
发送者
我们将修改发件者,以提供一种方法,通过使用RabbitTemplate的convertAndSend方法发布消息,以非常有创意的方式向邮件中添加点来识别其是否运行更长的任务。convertAndSend即“将Java对象转换为Amqp消息并将其发送到使用默认路由键的默认交换队列中”。
package com.example.rabbitmq;import org.springframework.amqp.core.Queue;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;/** * 消息发送者 * * Author: 王俊超 * Date: 2017-06-17 10:31 * All Rights Reserved !!! */public class Tut2Sender { /** 消息模板对象 */ @Autowired private RabbitTemplate template; /** 消息队列 */ @Autowired private Queue queue; private int dots = 0; private int count = 0; /** * 消息发送方法,初始延迟0.5秒,之后每1秒发送一个消息 */ @Scheduled(fixedDelay = 1000, initialDelay = 500) public void send() { StringBuilder builder = new StringBuilder("Hello"); if (dots++ == 3) { dots = 1; } for (int i = 0; i < dots; i++) { builder.append('.'); } builder.append(Integer.toString(++count)); String message = builder.toString(); template.convertAndSend(queue.getName(), message); System.out.println( " [x] Sent '" + message + "'"); }}
接收者
我们的接收器Tut2Receiver模拟了doWork()方法中的假任务的任意长度,其中点数转换为工作所需的秒数。再次,我们在“hello”队列和@RabbitHandler上使用@RabbitListener来接收消息。消费消息的实例被添加到我们的监视器中,以显示哪个实例,消息和处理消息的时间长度。
package com.example.rabbitmq;import org.springframework.amqp.rabbit.annotation.RabbitHandler;import org.springframework.amqp.rabbit.annotation.RabbitListener;import org.springframework.util.StopWatch;/** * 消息接收者对象,并指定接收的消息队列 * <p> * Author: 王俊超 * Date: 2017-06-17 10:30 * All Rights Reserved !!! */@RabbitListener(queues = "hello")public class Tut2Receiver { private final int instance; public Tut2Receiver(int i) { this.instance = i; } /** * 指定消息接收后的处理方法,因为发送的消息是string类型,所以接收方法的入参也是string类型 * * @param in * @throws InterruptedException */ @RabbitHandler public void receive(String in) throws InterruptedException { StopWatch watch = new StopWatch(); watch.start(); System.out.println("instance " + this.instance + " [x] Received '" + in + "'"); doWork(in); System.out.println("instance " + this.instance + " [x] Done in " + watch.getTotalTimeSeconds() + "s"); } private void doWork(String in) throws InterruptedException { for (char ch : in.toCharArray()) { if (ch == '.') { Thread.sleep(1000); } } }}
运行
先运行接收者,需要添加运行参数:–spring.profiles.active=hello-world,receiver
再运行发送者,需要添加运行参数:–spring.profiles.active=hello-world,sender
发件者的输出应该如下所示:
Ready ... running for 10000ms [x] Sent 'Hello.1' [x] Sent 'Hello..2' [x] Sent 'Hello...3' [x] Sent 'Hello.4' [x] Sent 'Hello..5' [x] Sent 'Hello...6' [x] Sent 'Hello.7' [x] Sent 'Hello..8' [x] Sent 'Hello...9' [x] Sent 'Hello.10'
而工作进程的输出应该如下所示:
Ready ... running for 10000msinstance 1 [x] Received 'Hello.1'instance 2 [x] Received 'Hello..2'instance 1 [x] Done in 1.001sinstance 1 [x] Received 'Hello...3'instance 2 [x] Done in 2.004sinstance 2 [x] Received 'Hello.4'instance 2 [x] Done in 1.0sinstance 2 [x] Received 'Hello..5'
消息确认
执行任务可能需要几秒钟。你可能会想,如果一个消费者开始一个长期的任务,并且仅仅部分地完成它,就会发生什么。默认情况下,Spring-amqp采用保守的消息确认方式。如果监听器引发异常,容器调用:
channel.basicReject(deliveryTag,requeue)
默认情况下,会重新排队,除非您明确设置:
defaultRequeueRejected = false
或者监听器抛出一个AmqpRejectAndDontRequeueException。这通常是你从接收者那里获得的一种典型行为。在这种模式下,没有必要担心忘记的确认。处理消息后,侦听器调用:
channel.basicAck()
忘记确认
错过basicAck是一个常见的错误,spring-amqp通过默认配置有助于避免这种情况。。这是一个容易的发生的错误,但后果是严重的。当您的客户端退出(可能看起来像随机重新传递)时,消息将被重新传递,但是RabbitMQ将会消耗越来越多的内存,因为它将无法释放任何未包含的消息。
为了调试这种错误,您可以使用rabbitmqctl 打印messages_unacknowledged字段:
sudo rabbitmqctl list_queues name message_ready messages_unacknowledgedged
在Windows上,删除sudo:
rabbitmqctl.bat list_queues name message_ready messages_unacknowledgedged
消息持久化
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。
当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非你不告诉它。需要两件事来确保消息不会丢失:我们需要将队列和消息标记为持久。
首先,我们需要确保RabbitMQ不会失去我们的队列。为了这样做,我们需要将其声明为持久的:
boolean durable = true ;channel.queueDeclare(“hello”,durable,false,false,null);
虽然这个命令本身是正确的,但是在我们目前的设置中是不行的。这是因为我们已经定义了一个非持久化的名为hello的队列。RabbitMQ不允许您重新定义具有不同参数的现有队列,并会向尝试执行此操作的任何程序返回错误。但是有一个快速的解决方法 - 让我们用不同的名称声明一个队列,例如task_queue:
boolean durable = true;channel.queueDeclare("task_queue", durable, false, false, null);
这个queueDeclare更改需要应用于生产者和消费者代码。
在这一点上,我们确信,即使RabbitMQ重新启动,task_queue队列也不会丢失。现在我们需要通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN来标记我们的消息
注意消息持久性
将消息标记为持久性不能完全保证消息不会丢失。虽然它告诉RabbitMQ将消息保存到磁盘,但是当RabbitMQ接受消息并且尚未保存消息时,仍然有一个很短的时间窗口。此外,RabbitMQ不会对每个消息执行fsync(2) - 它可能只是保存到缓存中,而不是真正写入磁盘。持久性保证不强,但对我们的简单任务队列来说已经足够了。如果您需要更强大的保证,那么您可以使用发布者确认。
公平调度
您可能已经注意到,调度仍然无法正常工作。例如在两个工人的情况下,当所有奇怪的信息都很重,甚至信息很轻的时候,一个工作人员将不断忙碌,另一个工作人员几乎不会做任何工作。那么,RabbitMQ不知道什么,还会平均分配消息。
这是因为当消息进入队列时,RabbitMQ只会分派消息。它不看消费者的未确认消息的数量。它只是盲目地向第n个消费者发送每个第n个消息。
为了打破这种方式,我们可以使用basicQos方法与 prefetchCount = 1设置。这告诉RabbitMQ不要一次给一个工作者多个消息。或者换句话说,在处理并确认前一个消息之前,不要向工作进程发送新消息。相反,它将发送到下一个还不忙的工作进程。
int prefetchCount = 1 ;channel.basicQos(prefetchCount);
注意队列大小
如果所有的工人都忙,你的队列可以填满。你会想要注意的是,也许增加更多的工人,或者有其他的策略
完整代码
我们的NewTask.java类的最终代码:
package com.example.rabbitmq;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.ConnectionFactory;import com.rabbitmq.client.MessageProperties;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * Author: 王俊超 * Date: 2017-06-09 08:08 * All Rights Reserved !!! */public class NewTask { private static final String TASK_QUEUE_NAME = "task_queue"; public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); String message = getMessage(args); channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8")); System.out.println(" [x] Sent '" + message + "'"); channel.close(); connection.close(); } private static String getMessage(String[] strings) { if (strings.length < 1) { return "Hello World!"; } return joinStrings(strings, " "); } private static String joinStrings(String[] strings, String delimiter) { int length = strings.length; if (length == 0) { return ""; } StringBuilder words = new StringBuilder(strings[0]); for (int i = 1; i < length; i++) { words.append(delimiter).append(strings[i]); } return words.toString(); }}
和我们的Worker.java:
package com.example.rabbitmq;import com.rabbitmq.client.*;import java.io.IOException;import java.util.concurrent.TimeoutException;/** * Author: 王俊超 * Date: 2017-06-09 08:02 * All Rights Reserved !!! */public class Worker { public static final String TASK_QUEUE_NAME = "task_queue"; public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); final Connection connection = factory.newConnection(); final Channel channel = connection.createChannel(); channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); // 指定从消息通道中每次取的消息数量 channel.basicQos(1); final Consumer consumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body, "UTF-8"); System.out.println(" [x] Received '" + message + "'"); try { doWork(message); } finally { System.out.println(" [x] Done"); channel.basicAck(envelope.getDeliveryTag(), false); } } }; channel.basicConsume(TASK_QUEUE_NAME, false, consumer); } private static void doWork(String task) { for (char ch : task.toCharArray()) { if (ch == '.') { try { Thread.sleep(1000); } catch (InterruptedException _ignored) { Thread.currentThread().interrupt(); } } } }}
使用消息确认和prefetchCount可以设置工作队列。即使RabbitMQ重新启动,耐久性选项也让任务生存下去。
- 04-rabbitmq-工作队列-spring
- RabbitMQ之工作队列
- RabbitMQ工作队列示例
- 03-rabbitmq-工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- RabbitMQ (二)工作队列
- rabbitmq 工作队列(java 实现)
- RabbitMQ (二)工作队列
- 其他算法
- 自定义图例点击事件
- NOIP2017模拟赛9
- 12319项目部署时遇到的相关问题
- 构造函数以及构造代码块的理解
- 04-rabbitmq-工作队列-spring
- 弗洛伊德算法得到图中任意两个顶点之间的最短路径
- android开发6/22记录
- 为什么使用U盘启动盘格式化系统盘没事
- iOS NSURLSessionConfiguration(配置)
- 如何编译ubuntu源码包里面的源码?
- CGridCtrl中调整滚动条到指定行(Row)
- 阿里巴巴开放平台学习
- TensorFlow batch normalization