在第一个教程里面,我们写了一个程序从一个有名字的队列中发送和接收消息,在这里我们将要创建一个分发耗时任务给多个worker的任务队列。

![](http://images2015.cnblogs.com/blog/658141/201608/658141-20160817001132015-1165677723.png)

任务队列核心思想就是避免执行一个资源密集型的任务,而程序要等待其执行完毕才能进行下一步的任务。相反地我们让任务延迟执行,我们封装一个task作为消息,并把它发送至队列,在后台运行的工作进程将弹出的任务,并最终执行作业。当运行多个worker的时候,task将在他们之间共享。

准备

在前一节中我们发送一个包含“HelloWorld!”的消息,现在我们发送字符串代表一个复杂的任务,我们没有一个真实的任务,比如格式化图片大小等等,所以我们使用Thread.sleep()代表一个执行时间较长的任务,这里我们使用几个点来代表任务的复杂度,每一个点代表任务执行一秒的时间,比如hello...就代表执行了3秒。

我们稍微改变一个上一节中的Send.java,允许从命令行发送任意的消息,程序将从我们的工作队列中执行任务,所以命名为NewTask.java:

String message = getMessage(argv);

channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");

以下是帮助从命令行参数获取消息体的代码:

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();
}

上一节中的Rece.java也需要少许改变:需要伪造一个根据点来执行多少秒的任务。它将处理传送过来的消息,并且执行任务,命名为Worker.java:

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.basicConsume(TASK_QUEUE_NAME, true, consumer);

模拟执行时间的任务:

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

编译:

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

循环调度

使用任务队列的优点之一就是很容并行化一个work,如果我们产生了工作积压,我们可以很简单的增加worker的数量,来解决问题。

首先,让我们尝试在同一时间运行两个工人实例。他们都将在队列中得到消息,但究竟如何?让我们来看看。

您需要三个控制台打开。两个将运行辅助程序。这些控制台将是我们的两名消费者 - C1和C2。

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

在第三个,我们将发布新的任务。一旦你开始运行消费者就可以发布几条消息:

shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask First message.
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Second message..
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Third message...
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fourth message....
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fifth message.....

我们来看看它是怎样将任务非配给我们的worker的:

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

默认情况下,RabbitMQ将发送每一个在序列中的消息到下一个消费者,平均而言,每一个消费者将获得相同数量的消息,发布这种消息的方式叫循环调度。

消息确认

做一个任务需要几秒钟,那么当一个消费者执行任务到一半的时候挂了怎么办?在我们的当前代码里面,一旦消息传送给我们的消费者,消息就从存储中删除了。在这种情况下,如果Kill了一个worker,我们不仅仅失去了它正在执行的消息任务,而且我们将失去所有分配给它,但是还没执行的消任务。

但是我们不想丢失任何消息,如果一个worker挂掉,我们将分配这些任务给其他的消费者。

为了确保消息不会丢失,RabbitMQ支持消息确认。一个ACK(nowledgement)从消费者发送给RabbitMQ一个消息确认当前消息已被接收和处理,RabbitMQ可自由将其删除。

如果消费者死亡(其信道被关闭,关闭连接,或TCP连接丢失),而不发送ACK,RabbitMQ知道消息并没有被接收和执行完全,将重新将它放入队列。如果同一时间存在其他在线的消费者,它将迅速重新传递消息给另一个消费者。这样,你可以肯定没有消息丢失,即使偶尔的消费者死亡。

目前没有任何消息超时,当消费者挂掉的时候,RabbitMQ将重新传递消息,即使处理一个消息需要很长很长的时间也没关系。

消息确认默认情况下开启。在前面的例子中,我们明确地通过AUTOA​​CK = true标志将它们关闭。现在是时候删除此标志,一旦我们与任务完成,将从worker发送适当的确认。

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);
}
}
};

使用此代码,我们可以肯定,即使你使用CTRL + C,杀死一个worker,什么都不会丢失。worker死亡后不久,所有未确认的消息会被重新传递。

被遗忘的确认

忘记baseACK是一个常见的错误,这是个简单的错误,但是后果是很严重的。当你的客户端退出的时候(可能看起来就像是随机交还)消息将被重新传递,但RabbitMQ会消耗的越来越多的内存,它将无法释放任何unacked的消息。

为了调试这种错误,你可以使用rabbitmqctl打印messages_unacknowledged字段。

$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

Listing queues ...

hello 0 0

...done.

消息持久化

我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果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。

import com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());

注意消息持久性

将消息标记为持久性并不能完全保证信息不会丢失。虽然RabbitMQ消息将保存到磁盘,但是有很短的时间内RabbitMQ已经接受了消息,但是还没来得及保存它。此外,RabbitMQ没有为每条消息做FSYNC(2) - 它可能只是保存到缓存,并没有真正写入磁盘。持久性的保证不强,但是对于我们简单的任务队列还是绰绰有余的。如果你需要一个更强有力的保证,那么你可以使用publisher confirms

公平调度

你可能已经注意到,调度仍然没有完全按照我们真正想要的工作。举个例子,比如有两个消费者的情况,当奇数的消息非常重,但是偶数的消息非常轻的时候,一个消费者将被累死,而另一个却闲着。RabbitMQ却不知道,仍然在均匀的给每个消费者发送消息。

这种情况发生是因为RabbitMQ只负责分发进入到队列的消息,它不看为消费者未确认的消息的数量。它只是盲目分派每第n个消息给第n消费者。

![](http://images2015.cnblogs.com/blog/658141/201608/658141-20160817001222234-1482830901.png)

为了杜绝那种情况,我们可以使用basicQos方法与prefetchCount = 1设置。它告诉RabbitMQ不要把多个消息在同一时间给一个消费者。或者,换句话说,只有消费者处理并且确认前一个消息之后才会给它分配下一个消息,相反,消息将被非配给下一个不处于忙碌的消费者。

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

注意队列大小

如果所有的worker都在忙,你的队列也填满了。您将要留意的是,也许添加更多的worker,或者有一些其他的策略。

代码整合

NewTask.java

import java.io.IOException;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties; public class NewTask { private static final String TASK_QUEUE_NAME = "task_queue"; public static void main(String[] argv)
throws java.io.IOException { 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(argv); channel.basicPublish( "", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
System.out.println(" [x] Sent '" + message + "'"); channel.close();
connection.close();
}
//...
}

Worker.java

import com.rabbitmq.client.*;

import java.io.IOException;

public class Worker {
private static final String TASK_QUEUE_NAME = "task_queue"; public static void main(String[] argv) throws Exception {
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();
}
}
}
}
}

原文地址:RabbitMQ之Work Queues

代码地址:https://github.com/aheizi/hi-mq

相关:

1.RabbitMQ之HelloWorld

2.RabbitMQ之任务队列

3.RabbitMQ之发布订阅

4.RabbitMQ之路由(Routing)

5.RabbitMQ之主题(Topic)

6.RabbitMQ之远程过程调用(RPC)

RabbitMQ之任务队列【译】的更多相关文章

  1. Scrapy使用RabbitMQ做任务队列

    前言 一个月没更博客了,这个月也搞了不少东西,但是公司对保密性要求挺高,很多东西都没有办法写出来 想来想去,还是写一篇最近写Scrapy中遇到的跳转问题 如果你的业务需求是遇到301/302/303跳 ...

  2. RabbitMQ之远程过程调用(RPC)【译】

    在第二个教程中,我们学习了如何使用工作队列在多个worker之间分配耗时的任务. 但是如果我们需要在远程计算机上运行功能并等待结果呢?嗯,这是另外一件事情,这种模式通常被称为远程过程调用(RPC). ...

  3. RabbitMQ之主题(Topic)【译】

    在上一节中,我们改进了我们的日志系统,替换使用fanout exchange仅仅能广播消息,使得选择性的接收日志成为可能. 虽然使用direct exchange改进了我们的系统,但是它仍然由他的局限 ...

  4. RabbitMQ之路由(Routing)【译】

    在上一节中,我们创建了一个简单的日志系统,可以广播消息到很多接收者. 这一节,我们将在上一节的基础上加一个功能--订阅部分消息.例如,我们只将严重错误信息写入到日志文件保存在磁盘上,同时我们能将所有的 ...

  5. RabbitMQ之发布订阅【译】

    在上一节中我们创建了一个工作队列,最好的情况是工作队列能够把任务恰到好处的分配给每一个worker.这一节中我们将做一些完全不同的事情--将消息传递给每一个消费者,这种模式被称为发布/订阅. 为了说明 ...

  6. RabbitMQ之HelloWorld【译】

    简介 RabbitMQ是一个消息代理,主要的想法很简单:它接收并转发消息.你可以把它当做一个邮局,当你发送邮件到邮筒,你相信邮差先生最终会将邮件投递给收件人.RabbitMQ在这个比喻里,是一个邮筒, ...

  7. python之celery的使用(一)

    前段时间需要使用rabbitmq做写缓存,一直使用pika+rabbitmq的组合,pika这个模块虽然可以很直观地操作rabbitmq,但是官方给的例子太简单,对其底层原理了解又不是很深,遇到很多坑 ...

  8. python之celery使用详解一

    前段时间需要使用rabbitmq做写缓存,一直使用pika+rabbitmq的组合,pika这个模块虽然可以很直观地操作rabbitmq,但是官方给的例子太简单,对其底层原理了解又不是很深,遇到很多坑 ...

  9. Scrapy分布式爬虫,分布式队列和布隆过滤器,一分钟搞定?

    使用Scrapy开发一个分布式爬虫?你知道最快的方法是什么吗?一分钟真的能 开发好或者修改出 一个分布式爬虫吗? 话不多说,先让我们看看怎么实践,再详细聊聊细节~ 快速上手 Step 0: 首先安装 ...

随机推荐

  1. 不敢想象!Vim使用者的“大脑”竟是这样

    原始状态 我曾经观看过小提琴家非常有激情地拉弦演奏,我有了这种想法:也许我投入到文本编辑器中的脑细胞数量和他为投入所喜好的乐器的演奏中差不多吧.我还有种奇异的想象,当他独奏的时候,脑中的核磁共振图和我 ...

  2. 【Caffe代码解析】Layer网络层

    Layer 功能: 是全部的网络层的基类,当中.定义了一些通用的接口,比方前馈.反馈.reshape,setup等. #ifndef CAFFE_LAYER_H_ #define CAFFE_LAYE ...

  3. IOS客户端Coding项目记录(二)

    9:第三方插件整理 JSON转实体:jsonModel https://github.com/icanzilb/JSONModel/ 美化按键:BButton https://github.com/m ...

  4. vlc模块间共享变量

    在模块中创建变量: vlc_value_t  valTemp; var_Create( p_intf, "vlc_test", VLC_VAR_STRING  ); valTemp ...

  5. jquery uploadify文件上传插件用法精析

      jquery uploadify文件上传插件用法精析 CreationTime--2018年8月2日11点12分 Author:Marydon 一.参数说明 1.参数设置 $("#fil ...

  6. 【划分树+二分】HDU 4417 Super Mario

    第一次 耍划分树.. . 模板是找第k小的 #include <stdio.h> #include <string.h> #include <stdlib.h> # ...

  7. 关于RHEL6下桥网配置的写法(ifcfg-eth0,ifcfg-br0) / 在阿里云的CentOS上安装docker

    Posted on 2011-07-28 16:46 zhousir1991 阅读(1978) 评论(0) 编辑 收藏 以下仅仅是我在做练习的时候下的环境,参照写即可:  [root@desktop2 ...

  8. 对固态硬盘ssd进行4k对齐

    别让SSD成半吊子!你真的4K对齐了吗? http://ssd.zol.com.cn/537/5374950_all.html SSD固态硬盘一键分区后如何检测4K对齐? http://pcedu.p ...

  9. OpenCV iOS开发(一)——安装(转)

    OpenCV是一个开源跨平台的的计算机视觉和机器学习库,可以用来做图片视频的处理.图形识别.机器学习等应用.本文将介绍OpenCV iOS开发中的Hello World起步. 安装 OpenCV安装的 ...

  10. sqlserver学习笔记(一)—— 登录本机sqlserver、启动和停止sqlserver服务、创建和删除数据库

    (重要参考:51自学网——SQL Server数据库教程) 首先按照网上教程安装好sqlserver,打开登录 登录本机sqlserver:①. ②localhost ③127.0.0.1 启动和停止 ...