RabbitMQ学习总结 第三篇:工作队列Work Queue
目录
RabbitMQ学习总结 第一篇:理论篇
RabbitMQ学习总结 第二篇:快速入门HelloWorld
RabbitMQ学习总结 第三篇:工作队列Work Queue
RabbitMQ学习总结 第四篇:发布/订阅 Publish/Subscribe
RabbitMQ学习总结 第六篇:Topic类型的exchange
RabbitMQ学习总结 第七篇:RCP(远程过程调用协议)
在上篇中我们实现了程序来从一个已经命名的队列里发送和接收消息。本篇博文中我们将要创建工作队列用来把一些比较耗时的任务分配给多个worker。
工作队列的主要思想就是避开立刻处理某个资源消耗交大的任务并且需要等待它执行完成。取而代之的是我们可以将它加入计划列表,并在后边执行这些任务。我们将任务分装成一个消息,并发送到队列中。后台的工作程序在接收到消息后将会立刻执行任务。当运行多个执行器时,任务将会在他们之间共享。
这个概念在web应用程序中是比较实用的,对于一些在一个短的http请求里无法完成的复杂任务。
1、准备
上篇博文中是发送一个包含”Hello World“的消息。现在我们来发送一条代表复杂任务的字符串。我们这里没有一个真实存在的任务,例如修改图片大小和渲染pdf文件这类的任务,这里我们模拟一个任务繁忙的场景(使用Thread.sleep()函数)。这里我们使用字符串类的点号个数来代表任务的复杂性,每一个点号都占用一秒钟的处理时间。例如,一个用”Hello…”来描述的伪造的任务将会占用三秒时间。
我们稍微修改一下上篇博文中的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 < )
return "Hello World!";
return joinStrings(strings, " ");
} private static String joinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == ) return "";
StringBuilder words = new StringBuilder(strings[]);
for (int i = ; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
旧的接收端也要做稍微的修改:消息体里的一个逗号代表一个一秒钟的任务,接收端会接收到消息,然后执行任务。这里重新命名为Work.java:
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody()); System.out.println(" [x] Received '" + message + "'");
doWork(message);
System.out.println(" [x] Done");
}
然后模拟执行任务消耗时间:
private static void doWork(String task) throws InterruptedException {
for (char ch: task.toCharArray()) {
if (ch == '.') Thread.sleep();
}
}
2、轮询调度
任务队列的一个较大的优势就是能够很方便的安排工作。如果后台队列里正在积压一些工作一直没有被执行的话,通过添加更多的worker就可以解决了。
首先,让我们来同时运行两个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
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
然后发布任务(运行发送端):
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会把每个消息以此轮询发到各个消费者那,把消息平均的发到各个消费者那。这种分配管理的方式叫轮询,还可以测试多个worker的情形。
3、消息应答机制
完成一个任务需要花费几秒钟。你一定很好奇,如果某个消费者开始执行某个任务花费了很长的时间并且在执行到某个部分时崩溃了那会怎么样。在我们现在的代码中,在向消费者推送某条消息后,RabbitMQ会立刻删除掉这条消息。这样的话,如果我们kill掉某个worker的话,那么我们将会流失掉该worker正在处理任务的消息(改任务未处理完成),我们也会丢失所有被发送到这个消费者且未处理完成的消息。
但是,我们不想丢失这部分消息,我们希望这类消息可以再次被发送到其它worker那。
为了保证永远不会丢失消息,RabbitMQ支持消息应答机制。当消费者接收到消息并完成任务后会往RabbitMQ服务器发送一条确认的命令,然后RabbitMQ才会将消息删除。
如果某个消费者在还有发送确认信息就挂了,RabbitMQ将会视为服务没有执行完成,然后把执行消息的服务再发给另外一个消费者。这种方式下,即时某个worker挂了,也不会使得消息丢失。
这里不是用超时来判断的,只有在某个消费者连接断开时,RabbitMQ才会把重新发送该消费者没有返回确认的消息到其它消费者那。即时处理某条任务花费了很长的时间,在这里也是没有问题的。
消息应答机制默认是打开的,在上边例子中我们明确的关闭了它(autoAck=true),那么现在应该如下修改程序:
QueueingConsumer consumer = new QueueingConsumer(channel);
boolean autoAck = false;
channel.basicConsume("hello", autoAck, consumer); while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//...
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
这样就可以保证即时你kill掉了worker也不会出现信息丢失的现象,worker被kill掉之后,所有的未确认消息将会被重新发送。
易错点:
很多人都会忘记调用basicAck方法,虽然这是一个很简单的错误,但往往却是致命。消费者退出后消息将会被重发,但是由于一些未能被确认消息不能被释放,RabbitMQ将会消耗掉越来越多的内存。
为了能够调试这种错误,你可以使用rabbitmqctl来打印出messages_unacknowledged字段。
$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello
...done.
4、消息的持久化
我们已经学习了在发生消费者挂掉或是任务被kill掉时的容错机制,下边将来看看当RabbitMQ服务被停止后,怎么保证消息不丢失。
当RabbitMQ退出或是宕机时会丢失队列和消息,当然有两个地方需要注意才能解决这类问题的发生:将队列和消息都持久化存储。
首先,我们要确保RabbitMQ永远不会丢失消息队列,那就需要声明它为持久化存储:
boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
虽然这里的操作是正确的,但在这里依然不会生效,因为命名为“hello”的队列在之前已经被创建(非持久化),现在已经存在了。RabbitMQ不允许你重新定义一个已经存在的消息队列,如果你尝试着去修改它的某些属性的话,那么你的程序将会报错。所以,这里你需要更换一个消息队列名称:
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
生产者和消费者都需要使用queueDeclare方法来指定持久化属性。
现在我们可以确保即使RabbitMQ重启了,任务队列也不会丢失。下边我就来实现消息持久化(通过设置属性MessageProperties. PERSISTENT_TEXT_PLAIN,其中MessageProperties实现了BasicProperties接口)。
import com.rabbitmq.client.MessageProperties; channel.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
标记消息持久化并不能百分百的保证消息一定不会被丢失,虽然RabbitMQ会把消息写到磁盘上,但是从RabbitMQ接收到消息到写到磁盘上,这个短时间的过程中发生的RabbitMQ重启依然会使得为写入到磁盘的消息被丢失。事实上是这样的,RabbitMQ接收到消息后,首先会把该消息写到内存缓冲区中,并不是直接把单条消息实时写到磁盘上的。消息的持久化不是健壮的,但是对于简单的任务队列是够用了。如果你需要一套很健壮的持久化方案,那么你可以使用publisher confirms(稍后会更新详细的使用方法)。
5、公平的任务分发策略
你可能会注意到有的时候RabbitMQ不能像你预想中的那样分发消息。例如有两个worker,第奇数个消息对应的任务都很耗时,第偶数个消息对应的任务都很快就能执行完。这样的话其中有个worker就会一直都很繁忙,另外一个worker几乎不做任务。RabbitMQ不会去对这种现象做任何处理,依然均匀的去推送消息。
这是因为RabbitMQ在消息被生产者推送过来后就被推送到消费者端,它不会去查看未接收到消费者确认的消息数量。它只会把N个消息均与的分发到N个消费者那。
为了能解决这个问题,我们可以使用basicQos放来来设置消费者最多会同时接收多少个消息。这里设置为1,表示RabbitMQ同一时间发给消费者的消息不超过一条。这样就能保证消费者在处理完某个任务,并发送确认信息后,RabbitMQ才会向它推送新的消息,在此之间若是有新的消息话,将会被推送到其它消费者,若所有的消费者都在处理任务,那么就会等待。
int prefetchCount = ;
channel.basicQos(prefetchCount);
注意消息队列的大小:
如果所有的worker都处于较忙的状态下,你的消息队列有可能会太长(出现内存或磁盘瓶颈)。需要尽量多的关注这些信息,出现的时候可以适当的添加worker。
6、代码的最后实现
发送端:
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();
}
//...
}
接收端:
import java.io.IOException;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer; public class Worker { private static final String TASK_QUEUE_NAME = "task_queue"; public static void main(String[] argv)
throws java.io.IOException,
java.lang.InterruptedException { ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
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(); QueueingConsumer consumer = new QueueingConsumer(channel); //打开消息应答机制
channel.basicConsume(TASK_QUEUE_NAME, false, consumer); while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody()); System.out.println(" [x] Received '" + message + "'");
doWork(message);
System.out.println(" [x] Done" ); //返回接收到消息的确认信息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
//...
}
使用消息应答机制和prefetchCount可以实现一个工作队列了。持久化的选项可以使任务即使队列和消息即使在RabbitMQ重启后,依然不会丢失。
关于Channel和MessageProperties的更多应用可以参考Java官方API文档:
http://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/
最后总结:
1、消费者端在信道上打开消息应答机制,并确保能返回接收消息的确认信息,这样可以保证消费者发生故障也不会丢失消息。
2、服务器端和客户端都要指定队列的持久化和消息的持久化,这样可以保证RabbitMQ重启,队列和消息也不会。
3、指定消费者接收的消息个数,避免出现消息均匀推送出现的资源不合理利用的问题。
参考链接:http://www.rabbitmq.com/tutorials/tutorial-two-java.html
RabbitMQ学习总结 第三篇:工作队列Work Queue的更多相关文章
- RabbitMQ学习总结 第四篇:发布/订阅 Publish/Subscribe
目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...
- RabbitMQ学习总结 第五篇:路由Routing
目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...
- RabbitMQ学习总结 第六篇:Topic类型的exchange
目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...
- 学习KnockOut第三篇之List
学习KnockOut第三篇之List 欲看此篇---------------------------------------------可先看上篇. 第一步,先搭建一个大概的框架起来 ...
- RabbitMQ学习系列(三): C# 如何使用 RabbitMQ
上一篇已经讲了Rabbitmq如何在Windows平台安装,还不了解如何安装的朋友,请看我前面几篇文章:RabbitMQ学习系列一:windows下安装RabbitMQ服务 , 今天就来聊聊 C# 实 ...
- 我们一起学习WCF 第三篇头消息验证用户身份
前言:今天我主要写的是关于头消息的一个用处验证用户信息 下面我画一个图,可以先看图 第一步:我们先开始做用户请求代码 首先:创建一个可执行的上下文对象块并定义内部传输的通道 using (Operat ...
- Egret入门学习日记 --- 第三篇 (书中 3.4 内容)
第三篇 (书中 3.4 内容) 今天还是要把昨天项目运行后,EXML文件里的界面没有出现的问题解决了才行. 去了群里,没人回.去了官网看文档,看不懂. 不过倒是看到了一个好东西: 还挺便宜啊,一个月要 ...
- RabbitMQ学习笔记(三) 发布与订阅
发布与订阅 在我们使用手机发送消息的时候,即可以选择给单个手机号码发送消息,也可以选择多个手机号码,群发消息. 前面学习工作队列的时候,我们使用的场景是一个消息只能被一个消费者程序实例接收并处理,但是 ...
- RabbitMQ学习笔记(二) 工作队列
什么是工作队列? 工作队列(又名任务队列)是RabbitMQ提供的一种消息分发机制.当一个Consumer实例正在进行资源密集任务的时候,后续的消息处理都需要等待这个实例完成正在执行的任务,这样就导致 ...
随机推荐
- 洛谷 P1038 神经网络 Label:拓扑排序 && 坑 60分待查
题目背景 人工神经网络(Artificial Neural Network)是一种新兴的具有自我学习能力的计算系统,在模式识别.函数逼近及贷款风险评估等诸多领域有广泛的应用.对神经网络的研究一直是当今 ...
- 2015 CTSC & APIO滚粗记
o诶人太弱..... 记一发滚粗记以便治疗我的健忘症= = //文章会不定时修改,添加一些内容什么的...因此最好看一下刷新一下(因为有可能你正在看= =我正在写... 5.2 早上9点坐上长达11小 ...
- 【BZOJ】2693: jzptab
http://www.lydsy.com/JudgeOnline/problem.php?id=2693 题意:求$\sum_{i=1}^{n} \sum_{j=1}^{m} lcm(i, j)$, ...
- POJ 1474 Video Surveillance(半平面交)
题目链接 2Y,模版抄错了一点. #include <cstdio> #include <cstring> #include <string> #include & ...
- 不错的判断 UITextView 内容不超过20个字符串的方法
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSSt ...
- GO语言练习:struct基础练习
1.代码 2.运行 1.代码 package main import "fmt" type Rect struct { x, y float64 width, height flo ...
- Hibernate笔试总结
1.在Hibernate中,以下关于主键生成器说法错误的是(AC). A.increment可以用于类型为long.short或byte的主键. B.identity用于如SQL Server.DB2 ...
- 修改wamp默认网站目录
使用WAMP集成环境,如何更改web根目录 做php开发使用WAMP集成环境的同学大部分有过这样的经历: 如果你试图修改web根目录,那么你肯定会想到要修改apache/apache2.2.11/co ...
- sql 执行 delete 的时候,结合子查询 exists ,怎样支持别名呢?
在做一个数据删除的时候,条件需要用到关联其他表,用到子查询,但是查询的时候使用 别名 没有问题,但是删除就有语法错误,在网上查询后得到了完美解决: --查询出来需要删除的数据 select * fro ...
- js在输出时乱码
如果网页头是<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> ...