工作队列

(使用Java客户端)

在这第一指南部分,我们写了通过同一命名的队列发送和接受消息。在这一部分,我们将会创建一个工作队列,在多个工作者之间使用分布式时间任务。 工作队列(亦称:任务队列)背后主要的思想是避免立即处理一个资源密集型任务并且不得不一直等待完成。相反我们可以计划着让任务后续执行。我们将任务封装 成消息,发送到队列中。一个工作者进程在后台运行,获取任务并最终执行任务。当你运行多个工作者,所有的任务将会被他们所共享。

在web应用程序中,这个理念是特别有用的,你无法在一个短暂的http请求中处理一个复杂的任务。

准备

在先前的指南中,我们发送了一个包含"Hello World!"消息。现在我们将要发送一些字符串,用来代表复杂的任务。我们没有一个真实的任务,比如图片的调整大小或者pdf文件渲染,所以我们通过Thread.sleep()函数,伪装一个我们是很忙景象。我们将会把字符串中点的数量来代表它的复杂度;每一个点将要花费一秒的工作。例如,一个使用Hello...描述的假任务会发送三秒。

我们将会轻量的修改我们以前例子中Send.java代码,使其允许任意的消息可以通过命令行发出。这个程序将要计划安排任务到我们的工作队列中,所以我们把它命名为NewTask.java:

  1. String message = getMessage(argv);
  2. channel.basicPublish("", "hello", null, message.getBytes());
  3. System.out.println(" [x] Sent '" + message + "'");

一些帮助从命令行中获取消息参数:

  1. private static String getMessage(String[] strings){
  2. if (strings.length < 1)
  3. return "Hello World!";
  4. return joinStrings(strings, " ");
  5. }
  6. private static String joinStrings(String[] strings, String delimiter) {
  7. int length = strings.length;
  8. if (length == 0) return "";
  9. StringBuilder words = new StringBuilder(strings[0]);
  10. for (int i = 1; i < length; i++) {
  11. words.append(delimiter).append(strings[i]);
  12. }
  13. return words.toString();
  14. }

我们老的Recv.java程序也要求做些改变:它需要将消息体中每个点伪装成一秒。从队列中获取消息,运行任务,所以我们将它称之为Worker.java:

  1. while (true) {
  2. QueueingConsumer.Delivery delivery = consumer.nextDelivery();
  3. String message = new String(delivery.getBody());
  4. System.out.println(" [x] Received '" + message + "'");
  5. doWork(message);
  6. System.out.println(" [x] Done");
  7. }

我们伪装的任务中冒充执行时间:

  1. private static void doWork(String task) throws InterruptedException {
  2. for (char ch: task.toCharArray()) {
  3. if (ch == '.') Thread.sleep(1000);
  4. }
  5. }

在第一部分指南中那样编译它们(jar 文件需要再工作路径上):

  1. $ javac -cp rabbitmq-client.jar NewTask.java Worker.java

循环分派

使用任务队列的优势之一是我们是容易并行处理。如果我们正在处理一些堆积的文件的话,我们仅仅需要增加更多的工作者,通过这种方式我们是容易扩展的。 首先,让我们试着在同一时间运行两个工作者实例。他们都会从队列中获取消息,但是具体怎样做呢?让我们一起来看一看。 你需要三个打开的控制平台,其中两个用来运行工作者程序。他们将会是我们的两个消费者-C1和C2。

  1. shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  2. Worker
  3. [*] Waiting for messages. To exit press CTRL+C
  1. shell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  2. Worker
  3. [*] Waiting for messages. To exit press CTRL+C

在这第三个控制平台我们用来发布新的任务。一旦你启动消费者,你就可以发布消息了:

  1. shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  2. NewTask First message.
  3. shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  4. NewTask Second message..
  5. shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  6. NewTask Third message...
  7. shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  8. NewTask Fourth message....
  9. shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  10. NewTask Fifth message.....

让我们看看什么被投递到我们工作者那里:

  1. shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  2. Worker
  3. [*] Waiting for messages. To exit press CTRL+C
  4. [x] Received 'First message.'
  5. [x] Received 'Third message...'
  6. [x] Received 'Fifth message.....'
  1. shell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
  2. Worker
  3. [*] Waiting for messages. To exit press CTRL+C
  4. [x] Received 'Second message..'
  5. [x] Received 'Fourth message....'

默认情况想,RabbitMQ将会把每一个消息发送给下一个消费者。平均下来每个消费者获取的消息数量是相同的。这种分布式消息方式被称为轮询。试试三个或更多的工作者。

消息确认

处理一个任务可能花费数秒时间,你可能会好奇如果一个消费者开始一个长任务,并且在处理完成部分的情况下就死掉了会发生什么情况。就我们当前的代码 来说,一旦RabbitMQ将消息传递给消费者,它就会立即将消息从内存中删除。在这种情况下,如果你杀掉一个正在处理的工作者你会丢失它正在处理的消 息。我们也同时失去了已经分配给这个工作者并且没有开始处理的消息。 但是我们不想丢失任何任务,如果一个工作者死掉,我们期望将任务传递给另一个工作者。 为了保证每一个消息不会丢失,RabbitMQ支持消息确认机制。一个消息确认是由消费者发出,告诉RabbitMQ这个消息已经被接受,处理完 成,RabbitMQ 可以删除它了。 如果一个消费者没有发送确认信号,RabbitMQ将会认定这个消息没有完全处理成功,将会把它传递给另一个消费者。通过这种方式,即使工作者有时会死 掉,你依旧可以保证没有消息会被丢失。 这里不存在消息超时;RabbitMQ只会在工作者连接死掉才重新传递这个消息。即使一个消息要被处理很长很长时间,也不是问题。 消息确认机制默认情况下是开着的。在先前的例子中我们是明确的将这个功能关闭no_ack=True。是时候移除这个标识了,一旦我们完成一个任务,工作者需要发送一个确认信号。

  1. QueueingConsumer consumer = new QueueingConsumer(channel);
  2. boolean autoAck = false;
  3. channel.basicConsume("hello", autoAck, consumer);
  4. while (true) {
  5. QueueingConsumer.Delivery delivery = consumer.nextDelivery();
  6. //...
  7. channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
  8. }

使用这段代码,我们可以保证即使你将一个正在处理消息的工作者通过CTRL+C来终止它运行,依旧没有消息会丢失。稍后,工作者死亡后没有发送确认的消息会被重新传递。

忘掉确认

这是一个普遍的错误,就是忘记确认。这是一个很简单的错误,但是这后果是严重的。当你的客户端退出,消息会重新传递(看上去是随机传递的),RabbitMQ会越来越占用内存,因为它不会释放哪些没有发送确认的消息。

为了调试这种类型的错误,你可以使用rabbitmqctl打印出messages_unacknowledged属性:

  1. $ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
  2. Listing queues ...
  3. hello 0 0
  4. ...done.

消息持久化

我们已经学习了如何在确定消费者是否已经死掉,并且保证任务不被丢失。但是如果RabbitMQ服务器停止,我们的任务依旧会丢失。

当RabbitMQ退出或者崩溃,它将会忘记这队列和消息,除非你告诉它不要这样做。两个事情需要做来保证消息不会丢失:我们标记队列和消息持久化。

首先,我们需要确保RabbitMQ不会丢失我们的队列,为了这样做,我们需要将它声明为持久化:

  1. boolean durable = true;
  2. channel.queueDeclare("hello", durable, false, false, null);

虽然这命令是正确的,但它不会立即在我们的程序里运行。那是因为我们已经定义了一个不持久化的hello队列。RabbitMQ不允许你使用不同的参数重新定义一个存在的队列,如果你试着那样做它会返回一个错误。有个快速的变通方案-让我们声明一个不同名字的队列,比如task_queue:

  1. boolean durable = true;
  2. channel.queueDeclare("task_queue", durable, false, false, null);

这个queuqDeclare的改变需要应用在生产者和消费者的代码中。 在这点上,我们可以保证即使RabbitMQ重启,task_queue队列也不会丢失。现在我们需要标记消息持久化 - 通过设置MessageProperties(实现了BasicProperties)的值为PERSISTENT_TEXT_PLAIN

  1. import com.rabbitmq.client.MessageProperties;
  2. channel.basicPublish("", "task_queue",
  3. MessageProperties.PERSISTENT_TEXT_PLAIN,
  4. message.getBytes());

注意消息持久化 标记消息持久化不能完全保证消息不会被丢失,虽然这样会告诉RabbitMQ保存消息到硬盘上。但是对于RabbitMQ依旧有个短暂的时间窗口对于接收 一个消息并且还没有完成保存。同样,RabbitMQ不能让每个消息同步--它可能仅仅保存在缓存中,还没有真正的写入到硬盘中。这持久化的保证不是健壮 的,但是对我们的简单的任务队列来说是足够了。如果你需要更健壮的持久化保证,你可以使用出版者确认。

公平分发

你可能注意到了,分发过程并没有如我们想的那样运作。例如,在某一种情况下有两个工作者,当所有奇数消息是很多的并且所有偶数的是少量的,一个工作者会一直忙碌下去,而另一个则会几乎不做什么事情。好吧,RabbitMQ不会在意那个事情,它会一直均匀的分发消息。 这种情况发生因为RabbitMQ仅仅分发消息到队列中。它不关心有多少消息没有由发送者发送确认信号。它仅仅盲目的将N个消息发送到N个消费者。

为了解决这个问题,我们可以使用basicQos方法,设置prefetchCount=1。这样将会告知RabbitMQ不要同时给一个工作者超过一个任务,或者换句话说在一个工作者处理完成,发送确认之前不要给它分发一个新的消息。代替,把消息分发到下一个不繁忙的工作者。

  1. int prefetchCount = 1;
  2. channel.basicQos(prefetchCount);

注意队列大小

如果你的所有工作者是在忙碌,你的队列就会被填满。你将会想关注这件事,可能要添加更多的工作者,或者有些其他策略。

把它们放在一起

我们的NewTask.java最终代码:

  1. import java.io.IOException;
  2. import com.rabbitmq.client.ConnectionFactory;
  3. import com.rabbitmq.client.Connection;
  4. import com.rabbitmq.client.Channel;
  5. import com.rabbitmq.client.MessageProperties;
  6. public class NewTask {
  7. private static final String TASK_QUEUE_NAME = "task_queue";
  8. public static void main(String[] argv)
  9. throws java.io.IOException {
  10. ConnectionFactory factory = new ConnectionFactory();
  11. factory.setHost("localhost");
  12. Connection connection = factory.newConnection();
  13. Channel channel = connection.createChannel();
  14. channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
  15. String message = getMessage(argv);
  16. channel.basicPublish( "", TASK_QUEUE_NAME,
  17. MessageProperties.PERSISTENT_TEXT_PLAIN,
  18. message.getBytes());
  19. System.out.println(" [x] Sent '" + message + "'");
  20. channel.close();
  21. connection.close();
  22. }
  23. //...
  24. }

(NewTask.java source) 我们的Worker.java代码:

  1. import java.io.IOException;
  2. import com.rabbitmq.client.ConnectionFactory;
  3. import com.rabbitmq.client.Connection;
  4. import com.rabbitmq.client.Channel;
  5. import com.rabbitmq.client.QueueingConsumer;
  6. public class Worker {
  7. private static final String TASK_QUEUE_NAME = "task_queue";
  8. public static void main(String[] argv)
  9. throws java.io.IOException,
  10. java.lang.InterruptedException {
  11. ConnectionFactory factory = new ConnectionFactory();
  12. factory.setHost("localhost");
  13. Connection connection = factory.newConnection();
  14. Channel channel = connection.createChannel();
  15. channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
  16. System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
  17. channel.basicQos(1);
  18. QueueingConsumer consumer = new QueueingConsumer(channel);
  19. channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
  20. while (true) {
  21. QueueingConsumer.Delivery delivery = consumer.nextDelivery();
  22. String message = new String(delivery.getBody());
  23. System.out.println(" [x] Received '" + message + "'");
  24. doWork(message);
  25. System.out.println(" [x] Done" );
  26. channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
  27. }
  28. }
  29. //...
  30. }

(Worker.java source) 使用消息确认和预读数量你可以建立一个工作队列。持久化选项使得RabbitMQ重启之后任务依旧存在。

想要了解更多关于通道方法和消息属性,你可以浏览javadocs online

现在我们可以移到指南3了,学习怎么样将相同的消息传递给多个消费者

转载RabbitMQ入门(2)--工作队列的更多相关文章

  1. RabbitMQ入门:工作队列(Work Queue)

    在上一篇博客<RabbitMQ入门:Hello RabbitMQ 代码实例>中,我们通过指定的队列发送和接收消息,代码还算是比较简单的. 假设有这一些比较耗时的任务,按照上一次的那种方式, ...

  2. RabbitMQ入门教程——工作队列

    什么是工作队列 工作队列是为了避免等待一些占用大量资源或时间操作的一种处理方式.我们把任务封装为消息发送到队列中,消费者在后台不停的取出任务并且执行.当运行了多个消费者工作进程时,队列中的任务将会在每 ...

  3. 转载RabbitMQ入门(6)--远程调用

    远程过程调用(RPC) (使用Java客户端) 在指南的第二部分,我们学习了如何使用工作队列将耗时的任务分布到多个工作者中. 但是假如我们需要调用远端计算机的函数,等待结果呢?好吧,这又是另一个故事了 ...

  4. 转载RabbitMQ入门(3)--发布和订阅

    发布和订阅 (使用java 客户端) 在先前的指南中,我们创建了一个工作队列.这工作队列后面的假想是每一个任务都被准确的传递给工作者.在这部分我们将会做一些完全不同的事情–我们将一个消息传递给多个消费 ...

  5. 转载RabbitMQ入门(1)--介绍

    目录[-] "Hello World" (使用java客户端) 发送 接收 把所有放在一起 前面声明本文都是RabbitMQ的官方指南翻译过来的,由于本人水平有限难免有翻译不当的地 ...

  6. RabbitMQ入门(2)——工作队列

    前面介绍了队列接收和发送消息,这篇将学习如何创建一个工作队列来处理在多个消费者之间分配耗时的任务.工作队列(work queue),又称任务队列(task queue). 工作队列的目的是为了避免立刻 ...

  7. 转载RabbitMQ入门(5)--主题

    主题(topic) (使用Java客户端) 在先前的指南中我们改进了我们的日志系统.取代使用fanout类型的交易所,那个仅仅有能力实现哑的广播,我们使用一个direct类型的交易所,获得一个可以有选 ...

  8. 转载RabbitMQ入门(4)--路由

    路由 (使用Java客户端) 在先前的指南中,我们建立了一个简单的日志系统.我们可以将我们的日志信息广播到多个接收者. 在这部分的指南中,我们将要往其中添加一个功能-让仅仅订阅一个消息的子集成为可能. ...

  9. RabbitMQ入门:总结

    随着上一篇博文的发布,RabbitMQ的基础内容我也学习完了,RabbitMQ入门系列的博客跟着收官了,以后有机会的话再写一些在实战中的应用分享,多谢大家一直以来的支持和认可. RabbitMQ入门系 ...

随机推荐

  1. Unsupervised Learning: Use Cases

    Unsupervised Learning: Use Cases Contents Visualization K-Means Clustering Transfer Learning K-Neare ...

  2. PE文件结构详解(六)重定位

    前面两篇 PE文件结构详解(四)PE导入表 和 PE文件结构详解(五)延迟导入表 介绍了PE文件中比较常用的两种导入方式,不知道大家有没有注意到,在调用导入函数时系统生成的代码是像下面这样的: 在这里 ...

  3. hdu 2940

    简单的大数乘法,直接改16进制~~ #include <cstdio> #include <cstdlib> #include <cmath> #include & ...

  4. uva 11029

    看了别人的解法 发现了 modf 这个函数 取小数部分 /*********************************************************************** ...

  5. 【C++基础】构造函数

    说说你对构造函数的理解? 构造函数:对象创建时,利用特定的值构造对象(不是构造类),将对象初始化(保证数据成员有初始值),是类的一个public 函数 ①   与类同名 ②   无返回值 ③   声明 ...

  6. Android开发--Activity生命周期回顾理解

    Activity和Servlet一样,都用了回调机制.我们通过类比servlet来学习Activity.当一个servlet开发出来之后,该servlet运行于Web服务器中.服务器何时创建servl ...

  7. c# 组元(Tuple)

    组元是C# 4.0引入的一个新特性,编写的时候需要基于.NET Framework 4.0或者更高版本.组元使用泛型来简化一个类的定义. 先以下面的一段代码为例子: public class Poin ...

  8. Windows JDK环境变量的配置

    下载JDK:http://www.oracle.com/technetwork/java/javase/downloads/index.html 安装 计算机-->属性-->高级系统设置- ...

  9. spring webservice 开发demo (实现基本的CRUD 数据库采用H2)

    在实现过程中,遇到两个问题: 1: schema 写错: 错误: http://myschool.com/schemas/st 正确: http://myschool.com/st/schemas   ...

  10. cojs 疯狂的魔法树 疯狂的颜色序列 题解报告

    疯狂的魔法树 一个各种操作大杂烩的鬼畜数据结构题目 首先我们注意到树的形态是半随机的 我们可以树分块,对树分成若干个块 对于每个块我们维护一个add标记表示增量 维护一个vis标记表示覆盖量 注意标记 ...