java工作队列_rabbitmq+java入门(二) 工作队列

参考:http://www.rabbitmq.com/tutorials/tutorial-two-java.html

源码:https://github.com/zuzhaoyue/JavaDemo

工作队列

(使用Java客户端)

先决条件

本教程假定RabbitMQ在标准端口(5672)上的本地主机上安装并运行。如果您使用不同的主机,端口或证书,则连接设置需要进行调整。

dc4bf1a70efbeb4c3ad95338754403cf.png

在第一篇教程中,我们编写了用于从已知的命名队列发送和接收消息的程序。而在本次教程中,我们将创建一个工作队列,用于向多个消费者分配耗时的任务。

工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待完成。相反,我们安排稍后完成任务。我们将任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享。

这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。

准备

在本教程的前一部分中,我们发送了一条包含“Hello World!”的消息。现在我们将发送代表复杂任务的字符串。我们没有真实的任务,比如要调整大小的图像或要渲染的PDF文件,所以让我们假装我们很忙 - 使用Thread.sleep()函数来伪装它。我们将把字符串中的"点数"作为它的复杂度;每一个点都会占用一秒的“工作”。例如,Hello ...描述的假任务将需要三秒钟。

我们稍微修改前面例子中的Send.java代码,以允许从命令行发送任意消息。这个程序将把任务安排到我们的工作队列中,所以让我们把它命名为NewTaskOrigin.java(为什么要加origin呢,因为后面还会进行改版,这个是初版):

String message = getMessage(argv);

channel.basicPublish("", "hello", null, message.getBytes());

System.out.println(" [x] Sent '" + message + "'");

getMessage方法如下,用于获取命令行的参数:

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

}

完整的NewTaskOrigin.java代码如下:

package rmq.workqueues;//package rmq.helloworld;/*** Created by zuzhaoyue on 18/5/15.*/

importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.Connection;importcom.rabbitmq.client.ConnectionFactory;public classNewTaskOrigin {private final static String QUEUE_NAME = "hello1";public static void main(String[] argv) throwsException {

ConnectionFactory factory= newConnectionFactory();

factory.setHost("localhost");

Connection connection=factory.newConnection();

Channel channel=connection.createChannel();

channel.queueDeclare(QUEUE_NAME,false, false, false, null);//String message = "Hello World!";

String message =getMessage(argv);

channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));

System.out.println(" [x] Sent '" + message + "'");

channel.close();

connection.close();

}//获取信息的方法,返回参数,若没有参数,则返回hello world

private staticString getMessage(String[] strings){if (strings.length < 1)return "Hello World!";return joinStrings(strings, " ");

}private staticString 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]);

}returnwords.toString();

}

}

我们的旧Recv.java程序也需要进行一些更改:它需要伪造邮件正文中每个点的成为时一秒的工作。它将处理交付的消息并执行任务,所以我们称之为WorkerOrigin.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");

}

}

};

boolean autoAck = true; // acknowledgment is covered below

channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);

其中dowork()的代码如下,它的功能是将每一个【点】都伪装成一秒执行时间的任务:

private static void doWork(String task) throws InterruptedException {

for (char ch: task.toCharArray()) {

if (ch == '.') Thread.sleep(1000);

}

}

完整的WorkerOrigin.java代码如下:

//package rmq.workqueues;

import com.rabbitmq.client.*;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/*** Created by zuzhaoyue on 18/5/15.*/

public classWorkerOrigin {private final static String QUEUE_NAME = "hello1";public static void main(String[] argv) throwsIOException,

InterruptedException, TimeoutException {

ConnectionFactory factory= newConnectionFactory();

factory.setHost("localhost");

Connection connection=factory.newConnection();

Channel channel=connection.createChannel();

channel.queueDeclare(QUEUE_NAME,false, false, false, null);

System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

Consumer consumer= newDefaultConsumer(channel) {

@Overridepublic void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)throwsIOException {

String message= new String(body, "UTF-8");

System.out.println(" [x] Received '" + message + "'");try{

doWork(message);

}catch(InterruptedException e) {

e.printStackTrace();

}finally{

System.out.println("[x] Done");

}

}

};boolean autoAck = true; //acknowledgment is covered below

String result =channel.basicConsume(QUEUE_NAME, autoAck, consumer);

System.out.println("result:" +result);

}//模拟执行任务的方法,一个点代表一秒

private static void doWork(String task) throwsInterruptedException {for (charch: task.toCharArray()) {if (ch == '.') Thread.sleep(1000);

}

}

}

循环调度

在上面的代码完成后,就可以来观察下rmq的循环调度了。

使用任务队列的优点之一是能够轻松地平行工作。如果我们正在积累积压的工作,我们可以增加更多的工作人员,并且这种方式很容易扩展。

首先,让我们试着同时运行三个消费者实例(原文是两个,但是我觉得三个看的更明显些)。他们都会从队列中获取消息,但具体到底是什么?让我们来看看。

您需要打开三个控制台来模拟三个消费者。本地的idea执行NewTaskOrigin.java作为生产者。

用java命令行启动消费者命令如下:

javac -cp /data/amqp-client-4.2.0.jar WorkerOrigin.java

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. WorkerOrigin

注:

1.jar包必须要有,不然会编译失败。

2.package那行代码要删除,不然会找不到主类。

在第三个我们将发布新的任务。一旦你启动了消费者(也就是启动了WorkerOrigin.java),你可以发布几条消息(启动NewTaskOrigin.java),命令如下:

javac -cp /data/amqp-client-4.2.0.jar NewTaskOrigin.java

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTaskOrigin 1 //发送第一条消息

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTaskOrigin 2.//发送第二条消息

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTaskOrigin 3..//发送第三条消息

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTaskOrigin 4...//发送第四条消息

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTaskOrigin 5....//发送第五条消息

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTaskOrigin 6.....//发送第六条消息

。。。

生产者发送消息的控制台显示如下:

9f8173eb987b34585efeab3eada275f4.png

让我们看看消费者那边的情况:

40789cba1d92c0843c9e356eb0b96111.png

ca2f5f511546e279a695b6aca52f9382.png

92fa18f0fac85aa8a17d2bec14fd3601.png

可以发现,消费者接收任务是按照顺序依次执行任务的,平均而言,每个消费者将获得相同数量的消息。这种分配消息的方式称为循环法。

消息确认

做任务可能需要几秒钟的时间。你可能想知道如果其中一个消费者开始一项长期任务并且只是部分完成或者直接挂了该怎么办。使用我们上面的代码,一旦RabbitMQ向客户发送消息,队列立即将其标记为删除。在这种情况下,如果你kill了一个消费者,我们将失去刚刚处理的信息,我们也会失去所有派发给这个特定消费乾但尚未处理的消息。但我们不想失去任何东西。如果一名消费者die了,我们希望将任务交付给另一个消费者。

为了确保消息永不丢失,RabbitMQ支持消息确认。消费者发回ack(nowledgement),告诉RabbitMQ收到了消息,并且RabbitMQ可以自由删除它。

如果消费者死亡(其通道关闭,连接关闭或TCP连接丢失),RabbitMQ将理解为该消息未被完全处理,并将这个消息重新排队。如果有其他消费者同时在线,它会迅速将其重新发送给另一位消费者。这样,即使消费者偶尔死亡,也可以确保没有任何信息丢失。

没有任何消息超时;当消费者死亡时,RabbitMQ将重新传递消息。即使处理消息需要非常很长的时间也没关系。

手动消息确认默认是打开。在前面的例子中,我们通过autoAck = true标志明确地关闭了它们。现在我们要把它标示为false,代表需要收到明确的确认ack.

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

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

System.out.println("[x] Done");

long tag = envelope.getDeliveryTag();//该消息的index

boolean multiple = false;//是否批量,true:一次性ack所有小于等于tag的消息,false:只ack index为tag的消息

channel.basicAck(tag, multiple);

}

}

};

// autoack改为false,打开manaul message ack

// autoack 值为true代表只要发出的消息都自动有一个ack

// 值false代表服务器会等待明确的ack,而不是自动返回的

// 英文版:

// true if the server should consider messages

// acknowledged once delivered;

// false if the server should expect

// explicit acknowledgements

boolean autoAck = false;

String result = channel.basicConsume(QUEUE_NAME, autoAck, consumer);

System.out.println("result:" + result);

使用这段代码,我们可以确定,即使在处理消息时使用CTRL + C来kill一个消费者,也不会丢失任何东西。消费者被kill后不久,所有未确认的消息将被重新发送。

确认必须在收到的传递的相同频道上发送。试图确认使用不同的通道会导致通道级协议异常。有关确认信息,请参阅文档指南以了解更多信息。

忘记确认

忽略basicAck是一个常见的错误。这是一个很简单的错误,但后果是严重的。当你的客户退出时(这可能看起来像随机的重新传送),消息将被重新传递,但是RabbitMQ会吃掉越来越多的内存,因为它不能释放任何未被ack的消息。

为了调试这种错误,您可以使用rabbitmqctl来打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

在Windows上,删除sudo:

rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged

消息持久性

我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。

当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非您告诉它不要。我们需要做两件事来确保消息不会丢失:我们需要将队列和消息标记为持久。

首先,我们需要确保RabbitMQ永远不会失去队列。为了做到这一点,我们需要宣布它是持久的:

booleandurable =true;channel.queueDeclare(“hello”,durable,false,false,null);

虽然这个命令本身是正确的,但它在我们目前的设置中不起作用。那是因为我们已经定义了一个名为hello的队列,这个队列并不是持久的。RabbitMQ不允许您使用不同的参数重新定义已有的队列,并会向任何试图执行该操作的程序返回错误。但是有一个快速的解决方法 - 让我们声明一个具有不同名称的队列,例如hello0517:

boolean durable =true ;channel.queueDeclare(“hello0517”,durable,false,false,null);

生产者和消费者的代码都需要更改queueDeclare这一段代码

此时我们确信,即使RabbitMQ重新启动,hello0517队列也不会丢失。

现在我们需要将消息标记为持久 - 将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。

importcom.rabbitmq.client.MessageProperties;

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

关于消息持久性的说明

将邮件标记为永久并不能完全保证消息不会丢失。尽管它告诉RabbitMQ将消息保存到磁盘,但RabbitMQ接收到消息并且尚未保存消息时仍有一段时间窗口。此外,RabbitMQ不会为每条消息执行fsync(2)- 它可能只是保存到缓存中,并没有真正写入磁盘。持久性保证不强,但对我们简单的任务队列来说已经足够了。如果您需要更强大的保证,那么您可以使用发布商确认。

公平派遣

以上保证了不会丢消息,但是调度仍然无法完全按照我们的想法工作。例如,在有两名消费者的情况下,当所有第3*n(原文是奇数个,因为他只用了两个消费者,而我用了三个消费者,所以我翻译为3*n)个工作都很重(需要很久),3*n + 1个工作很轻时,一名消费者会一直很忙,另一名消费者却几乎不会做任何工作。然而RabbitMQ不知道这些,并仍将均匀地发送消息。

发生这种情况是因为RabbitMQ只在消息进入队列时调度消息。它没有考虑消费者正在处理的消息的数量。它只是盲目地将第n条消息分发给第n个消费者。

f023f6cfd7d9ed431b90c072268c6925.png

为了解决这个问题,我们可以使用basicQos方法和prefetchCount=1设置。这告诉RabbitMQ一次不要向消费者发送多个消息。或者换句话说,不要向消费者发送新消息,直到它处理并确认了前一个消息。相反,它会将其分派给不是仍然忙碌的下一个消费者。

intprefetchCount =1;channel.basicQos(prefetchCount);

有关队列大小的说明

如果所有的消费者都很忙,你的队伍可能被填满。这时你也许可以增加更多的工人,或者也可能有其他的策略。

把以上放在一起

我们的NewTask.java类的最终代码:

//package rmq.workqueues;//package rmq.helloworld;/*** Created by zuzhaoyue on 18/5/17.*/

importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.Connection;importcom.rabbitmq.client.ConnectionFactory;importcom.rabbitmq.client.MessageProperties;public classNewTask {private final static String QUEUE_NAME = "hello0517";public static void main(String[] argv) throwsException {

ConnectionFactory factory= newConnectionFactory();

factory.setHost("localhost");

Connection connection=factory.newConnection();

Channel channel=connection.createChannel();boolean durable = true;//声明队列为持久类型的,声明的时候记得把队列的名字改一下,因为rmq不允许对一个已经存在的队列重新定义

channel.queueDeclare(QUEUE_NAME, durable, false, false, null);//String message = "Hello World!";

String message =getMessage(argv);

channel.basicPublish("", QUEUE_NAME,

MessageProperties.PERSISTENT_TEXT_PLAIN,//消息类型设置为persistece

message.getBytes("UTF-8"));

System.out.println(" [x] Sent '" + message + "'");

channel.close();

connection.close();

}//获取信息的方法,返回参数,若没有参数,则返回hello world

private staticString getMessage(String[] strings){if (strings.length < 1)return "Hello World!";return joinStrings(strings, " ");

}private staticString 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]);

}returnwords.toString();

}

}

Worker.java的完整代码如下:

//package rmq.workqueues;

import com.rabbitmq.client.*;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/*** Created by zuzhaoyue on 18/5/15.*/

public classWorker {private final static String QUEUE_NAME = "hello0517";public static void main(String[] argv) throwsIOException,

InterruptedException, TimeoutException {

ConnectionFactory factory= newConnectionFactory();

factory.setHost("localhost");

Connection connection=factory.newConnection();final Channel channel =connection.createChannel();boolean durable = true;

channel.queueDeclare(QUEUE_NAME, durable,false, false, null);

System.out.println(" [*] Waiting for messages. To exit press CTRL+C");int prefetchCount = 1;

channel.basicQos(prefetchCount);//代表让服务器不要同时给一个消费者超过1个消息,直到当前的消息被消耗掉

Consumer consumer= newDefaultConsumer(channel) {

@Overridepublic void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)throwsIOException {

String message= new String(body, "UTF-8");

System.out.println(" [x] Received '" + message + "'");try{

doWork(message);

}catch(InterruptedException e) {

e.printStackTrace();

}finally{

System.out.println("[x] Done");long tag = envelope.getDeliveryTag();//该消息的index

boolean multiple = false;//是否批量,true:一次性ack所有小于等于tag的消息,false:只ack index为tag的消息

channel.basicAck(tag, multiple);

}

}

};//autoack改为false,打开manaul message ack//autoack 值为true代表只要发出的消息都自动有一个ack//值false代表服务器会等待明确的ack,而不是自动返回的//英文版://true if the server should consider messages//* acknowledged once delivered;//false if the server should expect//* explicit acknowledgements

boolean autoAck = false;

String result=channel.basicConsume(QUEUE_NAME, autoAck, consumer);

System.out.println("result:" +result);

}//模拟执行任务的方法,一个点代表一秒

private static void doWork(String task) throwsInterruptedException {for (charch: task.toCharArray()) {if (ch == '.') Thread.sleep(1000);

}

}

}

使用消息确认和prefetchCount,您可以设置一个工作队列。即使RabbitMQ重新启动,耐用性选项也可让任务继续存在。

有关Channel方法和MessageProperties的更多信息,可以在线

测试

1.编译

javac -cp /data/amqp-client-4.2.0.jar Worker.java NewTask.java

打开三个窗口,在每个窗口执行以下命令:

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. Worker

这样就启动了三个消费者。

启动生产者发送消息:

java -cp /data/amqp-client-4.2.0.jar:/data/slf4j-api-1.7.21.jar:. NewTask 1..

如下图如示:

c8b0f7525d2d5d16ec81cf8d2da0059c.png

可以看到消费者不再循环遍历,而是有空的消费者会去消费队列里的任务:

11b7963072f521360efa7e562ccb3b3b.png

我们关闭正在执行任务的一个消费者,也会发现另一个消费者会马上执行这个任务,任务并没有丢失:

6c30de333e3fbc7cf942cc7f8a4384db.png

调试成功。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值