---恢复内容开始---

1、工作队列的简介

  在上一篇中,我们已经写了一个从已知队列中发送和获取消息的程序,在这里,我们创建一个工作队列(work queue), 会发送一些耗时的任务给多个工作者。模型图如下:

                      

  工作队列,由称为任务队列(task queue), 主要是为了避免一些占用大量资源,时间的操作。当我们把任务(task)当作消息发送到队列时, 一个运行在后台的工作者(worker),当你运行多个工作者,任务就会在它们之间共享。

  这个概念在网络应用中是非常有用的,它可以在短暂的HTTP请求中处理一些复杂的任务。

2、准备

  之前的教程中,我们发送了一个包含“Hello World!”的字符串消息。现在,我们将发送一些字符串,把这些字符串当作复杂的任务。我们没有真实的例子,例如图片缩放、pdf文件转换。所以使用time.sleep()函数来模拟这种情况。我们在字符串中加上点号(.)来表示任务的复杂程度,一个点(.)将会耗时1秒钟。比如"Hello..."就会耗时3秒钟。

  我们对之前教程的send.java做些简单的调整,以便可以发送随意的消息。这个程序会按照计划发送任务到我们的工作队列中。我们把它命名为NewTask.java,代码如下:

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection; import rabbitmq.utils.ConnectionUtils; public class NewTask { private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String[] messages = getMessage(args);
for(String message : messages) {
// 往队列里发送消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("Send '" + message + "'");
}
// 关闭通道
channel.close();
// 关闭连接
connection.close();
} private static String[] getMessage(String[] strings) {
if (strings.length < 1) {
strings = new String[] {
"1 message.",
"2 message..",
"3 message...",
"4 message.",
"5 message..",
"6 message...",
"7 message.",
"8 message..",
"9 message...",
"10 message.",
};
} return strings; } }

  我们的代码(receive.java)同样需要做一些改动:它需要为消息体中每一个点号(.)模拟1秒钟的操作。它会从队列中获取消息并执行,我们把它命名为worker1.java和worker2.java,这两个的代码时一摸一样的,代码如下:

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class Worker1 {
private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtils.getConnection();
// 创建 管道
Channel channel = connection.createChannel();
// 创建声明队列(可有可无)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received '" + message + "'");
try {
doWork(message);
}catch(Exception exception){
System.out.println(" [x] error");
}finally {
// System.out.println("Done");
}
} };
boolean autoAck = true; // acknowledgment is covered below
// 监听队列
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
} private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} }

3、循环调度(轮询调度)

  使用工作队列的一个好处就是它能够并行的处理队列。如果堆积了很多任务,我们只需要添加更多的工作者(workers)就可以了,扩展很简单。

首先,我们先同时运行worker1.java和worker2.java程序,它们都会从队列中获取消息,到底是不是这样呢?我们看看。需要打开三个终端,两个用来运行worker1.java和worker2.java程序,这两个终端就是我们的两个消费者(consumers)—— C1 和 C2。

  最后执行NewTask.java程序,其中效果如下:

  

  worker1的效果如下:

  

  worker2的效果如下:

  默认来说,RabbitMQ会按顺序得把消息发送给每个消费者(consumer)。平均每个消费者都会收到同等数量得消息。这种发送消息得方式叫做——轮询(round-robin)。

4、消息确认 

  当处理一个比较耗时得任务的时候,你也许想知道消费者(consumers)是否运行到一半就挂掉。当前的代码中,当消息被RabbitMQ发送给消费者(consumers)之后,马上就会在内存中移除。这种情况,你只要把一个工作者(worker)停止,正在处理的消息就会丢失。同时,所有发送到这个工作者的还没有处理的消息都会丢失。

  我们不想丢失任何任务消息。如果一个工作者(worker)挂掉了,我们希望任务会重新发送给其他的工作者(worker)。

  为了防止消息丢失,RabbitMQ提供了消息响应(acknowledgments)。消费者会通过一个ack(响应),告诉RabbitMQ已经收到并处理了某条消息,然后RabbitMQ就会释放并删除这条消息。

  如果消费者(consumer)挂掉了,没有发送响应,RabbitMQ就会认为消息没有被完全处理,然后重新发送给其他消费者(consumer)。这样,及时工作者(workers)偶尔的挂掉,也不会丢失消息。

  消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。

  消息响应默认是开启的。之前的例子中我们可以使用no_ack=True标识把它关闭。是时候移除这个标识了,当工作者(worker)完成了任务,就发送一个响应。

  在worker2代码中都做如下修改,当收到第7个或者第八个消息的工作者就挂掉,然后进行测试。

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class Worker2 {
private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtils.getConnection();
// 创建 管道
Channel channel = connection.createChannel();
// channel.basicQos(1); // accept only one unack-ed message at a time (see below)
// 创建声明队列(可有可无)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received '" + message + "'");
if(message.contains("7") || message.contains("8")) {
System.exit(0);
}
try {
doWork(message);
}catch(Exception exception){
System.out.println(" [x] error");
}finally {
// System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false); //给rabbitmq发送确认消息,我已经处理完任务了
}
} };
boolean autoAck = false; // acknowledgment is covered below
// 监听队列
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
} private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} }

work1的代码如下:

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class Worker1 {
private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtils.getConnection();
// 创建 管道
Channel channel = connection.createChannel();
// channel.basicQos(1); // accept only one unack-ed message at a time (see below)
// 创建声明队列(可有可无)
channel.queueDeclare(QUEUE_NAME, false, false, false, null); Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received '" + message + "'");
try {
doWork(message);
}catch(Exception exception){
System.out.println(" [x] error");
}finally {
// System.out.println("Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
} };
boolean autoAck = false; // acknowledgment is covered below
// 监听队列
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
} private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} }

  先执行work1和work2,然后执行,work2执行效果如下:

  

  当接收到第8个消息的时候就挂掉了,work1执行效果如下:

  

  work1还是收到了第8个消息。说明rabbitmq没有收到工作者发送到确认消息,是不会从队列中删除掉消息的,它会把消息发给另一个工作者处理。消息应答是默认打开的,即autoAck默认值是false。

 一个很容易犯的错误就是忘了basic_ack,后果很严重。消息在你的程序退出之后就会重新发送,如果它不能够释放没响应的消息,RabbitMQ就会占用越来越多的内存。channel.basicAck(envelope.getDeliveryTag(), false), 消息确认这行代码注意加上哦。

5、消息持久化

  如果你没有特意告诉RabbitMQ,那么在它退出或者崩溃的时候,将会丢失所有队列和消息。为了确保信息不会丢失,有两个事情是需要注意的:我们必须把“队列”和“消息”设为持久化。

  首先,为了不让队列消失,需要把队列声明为持久化(durable):

 boolean durable = true;   //声明持久化
// 创建队列
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

  尽管这行代码本身是正确的,但是仍然不会正确运行。因为我们已经定义过一个叫hello的非持久化队列。RabbitMq不允许你使用不同的参数重新定义一个队列,它会返回一个错误。但我们现在使用一个快捷的解决方法——用不同的名字,例如task_queue。

 channel.queueDeclare("task_queue", durable, false, false, null);

  在消费者中,也要在相应的代码中修,把队列名改为task_queue

  这时候,我们就可以确保在RabbitMq重启之后queue_declare队列不会丢失,即队列持久化。我们还要设置消息为持久性,在发送消息的时候,把消息设置为MessageProperties 的PERSISTENT_TEXT_PLAIN值就行,如下:

 // 往队列里发送消息
channel.basicPublish("",QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

  注意:消息持久化

  将消息设为持久化并不能完全保证不会丢失。以上代码只是告诉了RabbitMq要把消息存到硬盘,但从RabbitMq收到消息到保存之间还是有一个很小的间隔时间。因为RabbitMq并不是所有的消息都使用fsync(2)——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,你需要改写你的代码来支持事务(transaction)

6、公平调度

  你应该已经发现,它仍旧没有按照我们期望的那样进行分发。比如有两个工作者(workers),处理奇数消息的比较繁忙,处理偶数消息的比较轻松。然而RabbitMQ并不知道这些,它仍然一如既往的派发消息。

  这时因为RabbitMQ只管分发进入队列的消息,不会关心有多少消费者(consumer)没有作出响应。它盲目的把第n-th条消息发给第n-th个消费者。

                    

  我们可以使用basic.qos方法,并设置prefetch_count=1。这样是告诉RabbitMQ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ就会把消息分发给下一个空闲的工作者(worker)。在每个工作者中加如下代码:

 Channel channel = connection.createChannel();
channel.basicQos(1); // accept only one unack-ed message at a time (see below)

  

  注意关于队列大小:如果所有的工作者都处理繁忙状态,你的队列就会被填满。你需要留意这个问题,要么添加更多的工作者(workers),要么使用其他策略。

  所有的代码整合如下:

  NewTask.java

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties; import rabbitmq.utils.ConnectionUtils; public class NewTask { private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
boolean durable = true; //声明持久化
// 创建队列
channel.queueDeclare("task_queue", durable, false, false, null);
String[] messages = getMessage(args);
for(String message : messages) {
// 往队列里发送消息
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println("Send '" + message + "'");
}
// 关闭通道
channel.close();
// 关闭连接
connection.close();
} private static String[] getMessage(String[] strings) {
if (strings.length < 1) {
strings = new String[] {
"1 message.",
"2 message..",
"3 message...",
"4 message.",
"5 message..",
"6 message...",
"7 message.",
"8 message..",
"9 message...",
"10 message.",
};
} return strings; } }

  work1.java

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class Worker1 {
private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtils.getConnection();
// 创建 管道
Channel channel = connection.createChannel();
channel.basicQos(1); // accept only one unack-ed message at a time (see below)
// 创建声明队列(可有可无)
channel.queueDeclare(QUEUE_NAME, false, false, false, null); Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received '" + message + "'");
try {
doWork(message);
}catch(Exception exception){
System.out.println(" [x] error");
}finally {
// System.out.println("Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
} };
boolean autoAck = false; // acknowledgment is covered below
// 监听队列
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
} private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} }

  work2.java

 package rabbitmq.main;

 import java.io.IOException;
import java.util.concurrent.TimeoutException; import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope; import rabbitmq.utils.ConnectionUtils; public class Worker2 {
private static final String QUEUE_NAME = "rabbitmq_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionUtils.getConnection();
// 创建 管道
Channel channel = connection.createChannel();
channel.basicQos(1); // accept only one unack-ed message at a time (see below)
// 创建声明队列(可有可无)
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("Received '" + message + "'");
if(message.contains("7") || message.contains("8")) {
System.exit(0);
}
try {
doWork(message);
}catch(Exception exception){
System.out.println(" [x] error");
}finally {
// System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false); //给rabbitmq发送确认消息,我已经处理完任务了
}
} };
boolean autoAck = false; // acknowledgment is covered below
// 监听队列
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
} private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.')
Thread.sleep(1000);
}
} }

  执行效果如下:

  work1:

  

  work2

  

  我们可以看出,不是轮询分发了,谁先做完就发给谁,这种模式也叫能者多劳。

  

  

  

rabbitmq系列二 之工作队列的更多相关文章

  1. 在Node.js中使用RabbitMQ系列二 任务队列

    在上一篇文章在Node.js中使用RabbitMQ系列一 Hello world我有使用一个任务队列,不过当时的场景是将消息发送给一个消费者,本篇文章我将讨论有多个消费者的场景. 其实,任务队列最核心 ...

  2. RabbitMQ (二)工作队列

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/37620057 本系列教程主要来自于官网入门教程的翻译,然后自己进行了部分的修改与 ...

  3. RabbitMQ (二)工作队列 -摘自网络

    这篇中我们将会创建一个工作队列用来在工作者(consumer)间分发耗时任务.工作队列的主要任务是:避免立刻执行资源密集型任务,然后必须等待其完成.相反地,我们进行任务调度:我们把任务封装为消息发送给 ...

  4. python使用rabbitMQ介绍二(工作队列模式)

    一模式介绍 第一章节的生产-消费者模式,是非常简单的模式,一发一收.在实际的应用中,消费者有的时候需要工作较长的时间,则需要增加消费者. 队列模型: 这时mq实现了一下几个功能: rabbitmq循环 ...

  5. 轻松搞定RabbitMQ(二)——工作队列之消息分发机制

    转自 http://blog.csdn.net/xiaoxian8023/article/details/48681987 上一篇博文中简单介绍了一下RabbitMQ的基础知识,并写了一个经典语言入门 ...

  6. RabbitMQ系列(二)--基础组件

    声明:对于RabbitMQ的学习基于某课网相关视频和<RabbitMQ实战指南>一书,后续关于RabbitMQ的博客都是基于二者 一.什么是RabbitMQ RabbitMQ是开源代理和队 ...

  7. RabbitMQ系列二(构建消息队列)

    从AMQP协议可以看出,MessageQueue.Exchange和Binding构成了AMQP协议的核心.下面我们就围绕这三个主要组件,从应用使用的角度全面的介绍如何利用RabbitMQ构建消息队列 ...

  8. RabbitMQ系列教程之二:工作队列(Work Queues)(转载)

    RabbitMQ系列教程之二:工作队列(Work Queues)     今天开始RabbitMQ教程的第二讲,废话不多说,直接进入话题.   (使用.NET 客户端 进行事例演示)          ...

  9. RabbitMQ系列(二)深入了解RabbitMQ工作原理及简单使用

    深入了解RabbitMQ工作原理及简单使用 RabbitMQ系列文章 RabbitMQ在Ubuntu上的环境搭建 深入了解RabbitMQ工作原理及简单使用 RabbitMQ交换器Exchange介绍 ...

随机推荐

  1. 2018.07.20 bzoj3211: 花神游历各国(线段树)

    传送门 维护区间开方,区间求和.这个是线段树常规操作. 显然一个数被开方若干次之后要么是1,要么是0,所以用线段树维护区间最大和区间和,如果区间最大不超过1就剪枝剪掉,不然就继续递归直到叶节点时停下进 ...

  2. 手机PC文件传输

    QQ啥的现在直接无法全部退出,很纠结后台运行,时不时的来条消息,明明电脑QQ还开着,越来越流氓了. 服务端代码: <%@ Page Language="C#" %> & ...

  3. 在linux系统中安装VSCode(Visual Studio Code)和图标的创建方式

    本文转载自:https://www.cnblogs.com/lzpong/p/6145511.html,自己添加了一些关于依赖包安装的. 1.从官网下载压缩包(话说下载下来解压就直接可以运行了咧,都不 ...

  4. DIV+CSS实战(二)

    一.说明 在DIV+CSS实战(一)中,已经把框架搭建起来了,现在就需要往框架里面添加内容了.需要实现的内容如下图: 二.头部的设计(全媒体订阅) 左侧是一张图片+标题 ,右侧是登录名 和上次登录的时 ...

  5. No cache or cacheManager properties have been set. Authorization cache cannot be obtained.

    20235 [http-bio-8080-exec-10] INFO o.a.shiro.realm.AuthorizingRealm - No cache or cacheManager prope ...

  6. Windows10+Python3下安装NumPy+SciPy+Matplotlib

    Numpy.SciPy.MatplotLib是Python下从事科学计算必不可少的库.我在用其他的方法安装时出现各种问题,发现直接安装.whl包是最快且不报错的方法. 1.下载.whl包在下面的网站中 ...

  7. POI解析Excel文件

    @Test public void test1() throws FileNotFoundException, IOException{ HSSFWorkbook workbook = new HSS ...

  8. 计算服务器的pv量算法

    如何计算服务器能够承受多大的pv?   你想建设一个能承受500万PV/每天的网站吗? 500万PV是什么概念?服务器每秒要处理多少个请求才能应对?如果计算呢? PV是什么: PV是page view ...

  9. Hdu2181 哈密顿绕行世界问题 2017-01-18 14:46 45人阅读 评论(0) 收藏

    哈密顿绕行世界问题 Time Limit : 3000/1000ms (Java/Other)   Memory Limit : 32768/32768K (Java/Other) Total Sub ...

  10. hibernate 一对多,由谁维护性能最优

    举例如下 Customer类: public class Customer { private int id; private String name; private Set orders = ne ...