最初のチュートリアルでは、手順指定されたキューの送信を書いてからメッセージを受信します。このプロセスでは、我々は作業キューを作成し、複数の労働者の間で時間のかかるタスクを割り当てるために使用されます。
ワークキュー:主な考え方(別名タスクキュー)の背後には、すぐにリソースを大量に消費するタスクの実行を回避することであり、それが終了するのを待たなければなりません。代わりに、我々は将来的にタスクを完了するために手配します。私たちは、タスクとしてパッケージ化され、メッセージ・キューに送信します。バックグラウンドで実行中のワークプロセスがこれらのタスクをポップアップ表示し、最終的にタスクを実行します。あなたは、多くの消費者を実行すると、タスクは、それらの間で共有されます。
短いHTTPリクエストウィンドウで複雑なタスクを処理することは不可能であるため、この概念は、Webアプリケーションで特に有用です。
1、準備ができて
このチュートリアルの前のセクションでは、我々は含まれていたメッセージの送信に「Hello World!」というです。今、私たちは、複雑なタスクの文字列の送信をサポートします。()関数のThread.sleepを使用して、それをふりをする - 私たちは、それでは、私たちは非常に忙しいふりを聞かせて、画像やPDFファイルを調整するなど、実際の仕事を持っていません。私たちは、その複雑さなどの文字列にカウントされます。各点は、第二の「仕事」を占めることになります。たとえば、タスクが偽こんにちはについて説明しました。これは、3秒かかります。
私たちは、コマンドラインから送信されたすべてのメッセージを許可する、少し前の例から変更Send.javaのJavaコードを使用します。このプログラムは、その者の名前が、それはNewTask.java聞かせて、私たちの作業キューを手配するタスクになります。
NewTask.java
public class NewTask {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
for(int i = 1; i <= 6; i++) {
StringBuilder message = new StringBuilder("Hello World!");
Thread.sleep(2000);
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.append(i).toString().getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
channel.close();
connection.close();
}
}
我々の以前のRecv.javaプログラムはまた、いくつかの変更を必要とします。それは、メッセージ本体内の各ポイントの副業を偽造する必要があります。それは私たちがWorker.javaそれを呼び出すので、メッセージ処理を渡し、タスクを実行します。
Worker.java
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("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
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");
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) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
図2に示すように、周期分布メッセージ
簡単に並行して作業することができますタスクキューを使用する利点の1つ。私たちは多くの作業を蓄積している場合、我々はあなたが簡単に、スケールアップをスケールアウトできるように、より多くの労働者を追加することができます。
まずは、同時に2つのorkerインスタンスを実行してみましょう。彼らは、キューからメッセージを取得しますが、実行する方法の詳細されていますか?私たちは、6つのメッセージNewTask.javaの内部を送りました。
私たちは、これら2つの労働者がメッセージを受信する方法の例を見てみましょう:
消費者-1
消費者-2
デフォルトでは、RabbitMQのは、順次ダウンユーザは、各メッセージを送信します。平均的な消費者は、メッセージの同じ数を取得します。メッセージを分配するために、この方法では、サイクルと呼ばれています。読者は試してみて、一緒に消費者の3以上を試すことができます。
図3に示すように、メッセージ確認
数秒かかる場合がありますタスクを完了します。消費者は、長期的なタスクを開始し、そして唯一の仕事の一部を完了した場合はどうなるのか、不思議に思うかもしれません。私たちの現在のコードでは、一回のRabbitMQは削除のためにすぐにマーク顧客にメッセージを送信します。あなたは労働者を殺すこの場合、私たちは情報を処理し、それを失うことになります。また、この特定のスタッフに送信されたすべてのメッセージを失うが、まだ処理されていないでしょう。
しかし、我々は、任意のタスクを失いたくありません。労働者の死亡した場合、私たちは、この使命は、他の労働者に配信することにしたいです。
メッセージは、RabbitMQのサポートが失われないことを確実にするために、メッセージの受信確認を。ACK(nowledgement)は、それを削除するには、消費者に返されたのRabbitMQを告げ、特定のメッセージが受信された、処理され、RabbitMQの無料ました。
消費者が(そのチャネルがクローズされ、接続が閉じられる、またはTCP接続が失われた)死んでいる場合は、ACKを送信することなく、RabbitMQのメッセージが完全に処理されていない、とキューを再ます理解します。インターネット上の他の消費者が同時に存在する場合、それはすぐに別の顧客に再配信されます。でも労働者時折死かかわらず、あなたは何も情報が失われないようにすることができます。
いいえニュース残業ません。ユーザー死のRabbitMQは、メッセージを再送します。メッセージを処理するのに長い時間がかかる場合でも、それも可能です。
マニュアルの確認メッセージデフォルトでは開いています。前の例では、明示的でautoAck=true
自分のラベルを閉じます。今falseに設定されているこのフラグを置くための時間であり、タスクの完了後に労働者への適切な確認メッセージを送信します。
channel.basicQos(1); // accept only one unack-ed message at a time (see below)
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);
}
}
};
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);
このコードを使用して、我々は何もメッセージを処理するときに使用する場合でも、ワーカーインスタンスを殺し、あなたが失うことはありません、確認することができます。まもなく労働者の死の後、すべての情報が再配信されます確認されていません。
确认必须在接收到的同一通道上发送。尝试使用不同的通道进行确认将导致通道级协议异常。请参阅 文档指南以了解更多信息。
4、被遗忘的确认
忘记进行 acs 确认这是一个容易犯的错误,但后果是严重的。当你的客户端退出时,消息将被重新发送(这可能看起来像是随机的重新交付),但是 RabbitMQ 将会吃掉越来越多的内存,因为它将无法释放任何未被删除的消息。
为了调试这种错误,您可以使用 rabbitmqctl 打印 messages_unacknowledged 字段:
sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
在Windows上,sudo:
rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged
5、持久化消息
我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果 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
6、公平的分配
你可能已经注意到,分派仍然不能完全按照我们的要求工作。例如,在 worker 工作的情况下,当所有的奇怪信息都很重,甚至消息都很轻时,一个 worker 就会一直很忙,而另一个 worker 几乎不会做任何工作。好吧,RabbitMQ对此一无所知,仍然会均匀地发送消息。
之所以发生这种情况,是因为 RabbitMQ 在消息进入队列时才会发送一条消息。它不关注消费者未被认可的消息的数量。它只是盲目地将每一个 n 条信息发送给第 n 个消费者。
在这种情况下我们可以使用 basicQos
方法和 prefetchCount=1
设置。这告诉 RabbitMQ,不要一次给一个 worker 发送多个消息。或者,换句话说,在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它会把它分派给下一个不太忙的 worker。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
注意队列大小
如果所有的 worker 都很忙,你的队伍就会被填满。你会想要关注这一点,也许会增加更多的wroker,或者有其他的策略。
NewTask.java
public class NewTask {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
for(int i = 1; i <= 6; i++) {
StringBuilder message = new StringBuilder("Hello World!");
Thread.sleep(2000);
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.append(i).toString().getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
channel.close();
connection.close();
}
}
Worker.java
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("192.168.20.128");
factory.setPort(5672);
factory.setUsername("carl");
factory.setPassword("198918");
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();
}
}
}
}
}
使用消息确认和 prefetchCount
,你可以设置一个工作队列。即使 RabbitMQ 重新启动,持久性选项也能让任务存活下来。
メソッドとMessagePropertiesチャンネルの詳細については、Webを閲覧することができ、オンラインのJavaDocを。