rabbitmq+java入门(二) 工作队列
参考:http://www.rabbitmq.com/tutorials/tutorial-two-java.html
源码:https://github.com/zuzhaoyue/JavaDemo
工作队列
(使用Java客户端)
先决条件
本教程假定RabbitMQ 在标准端口(5672)上的本地主机上安装并运行。如果您使用不同的主机,端口或证书,则连接设置需要进行调整。
![](http://www.rabbitmq.com/img/tutorials/python-two.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. */ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; public class NewTaskOrigin { private final static String QUEUE_NAME = "hello1"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); 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 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(); } }
我们的旧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.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * Created by zuzhaoyue on 18/5/15. */ public class WorkerOrigin { private final static String QUEUE_NAME = "hello1"; public static void main(String[] argv) throws IOException, InterruptedException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); 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 = 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"); } } }; 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) throws InterruptedException { for (char ch: 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.....//发送第六条消息
。。。
生产者发送消息的控制台显示如下:
让我们看看消费者那边的情况:
![](https://images2018.cnblogs.com/blog/953701/201805/953701-20180517120934861-95860409.png)
![](https://images2018.cnblogs.com/blog/953701/201805/953701-20180517120948608-1916014532.png)
![](https://images2018.cnblogs.com/blog/953701/201805/953701-20180517121018346-1419833182.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永远不会失去队列。为了做到这一点,我们需要宣布它是持久的:
boolean durable = 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。
import com.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个消费者。
![](http://www.rabbitmq.com/img/tutorials/prefetch-count.png)
为了解决这个问题,我们可以使用basicQos方法和 prefetchCount = 1设置。这告诉RabbitMQ一次不要向消费者发送多个消息。或者换句话说,不要向消费者发送新消息,直到它处理并确认了前一个消息。相反,它会将其分派给不是仍然忙碌的下一个消费者。
int prefetchCount = 1 ;
channel.basicQos(prefetchCount);
有关队列大小的说明
如果所有的消费者都很忙,你的队伍可能被填满。这时你也许可以增加更多的工人,或者也可能有其他的策略。
把以上放在一起
我们的NewTask.java类的最终代码:
//package rmq.workqueues;//package rmq.helloworld; /** * Created by zuzhaoyue on 18/5/17. */ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MessageProperties; public class NewTask { private final static String QUEUE_NAME = "hello0517"; public static void main(String[] argv) throws Exception { ConnectionFactory factory = new ConnectionFactory(); 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 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(); } }
Worker.java的完整代码如下:
//package rmq.workqueues; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * Created by zuzhaoyue on 18/5/15. */ public class Worker { private final static String QUEUE_NAME = "hello0517"; public static void main(String[] argv) throws IOException, InterruptedException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); 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 = 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); } //模拟执行任务的方法,一个点代表一秒 private static void doWork(String task) throws InterruptedException { for (char ch: task.toCharArray()) { if (ch == '.') Thread.sleep(1000); } } }
使用消息确认和prefetchCount,您可以设置一个工作队列。即使RabbitMQ重新启动,耐用性选项也可让任务继续存在。
有关Channel方法和MessageProperties的更多信息,可以在线浏览 JavaDocs。
测试
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..
如下图如示:
可以看到消费者不再循环遍历,而是有空的消费者会去消费队列里的任务:
我们关闭正在执行任务的一个消费者,也会发现另一个消费者会马上执行这个任务,任务并没有丢失:
调试成功。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步