RabbitMQ文檔翻譯二(JAVA).工作隊列

在這裏插入圖片描述
第一個教程中,我們編寫了從命名隊列發送和接收消息的程序。在本例中,我們將創建一個工作隊列,用於在多個工作人員之間分配耗時的任務。

工作隊列(又名:任務隊列)的主要思想是避免立即執行資源密集型任務,並且必須等待任務完成。相反,我們把任務安排在以後完成。我們將任務封裝爲消息並將其發送到隊列。在後臺運行的工作進程將從隊列彈出(pop操作)任務並最終執行該作業。當您運行多個worker時,任務將在他們之間共享。

這個概念在web應用程序中特別有用,因爲在短的HTTP請求窗口期間無法處理複雜的任務。

準備

在本教程的前一部分中,我們發送了一條包含“Hello World!”的消息。現在我們將發送代表複雜任務的字符串。我們沒有實際的任務,比如要調整大小的圖像或要呈現的pdf文件,所以讓我們假裝很忙-通過使用Thread.sleep() 功能。我們將以字符串中的點的數量作爲其複雜性;每個點將佔“工作”的1秒。例如,一個由“Hello… ”描述的假任務需要三秒鐘。

我們將輕微修改上一個示例中的代碼Send.java,使其允許從命令行發送任意消息。這個程序會將任務調度到我們的工作隊列中,所以我們來命名它NewTask.java:

String message = String.join(" ", argv);

channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'"); 

我們的老朋友Recv.java程序也需要一些改變:它需要爲消息體中的每個點僞造一秒鐘的工作。它將處理傳遞的消息並執行任務,因此我們將其命名爲Worker.java:

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
  String message = new String(delivery.getBody(), "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, deliverCallback, consumerTag -> { });

我們模擬執行時間的假任務:

private static void doWork(String task) throws InterruptedException {
    for (char ch: task.toCharArray()) {
        if (ch == '.') Thread.sleep(1000);
    }
}

按照教程一(見上一篇)中的方法編譯它們(使用工作目錄中的jar文件和環境變量CP):

javac -cp $CP NewTask.java Worker.java

輪詢分發

使用任務隊列的優點之一是能夠輕鬆地並行處理工作。如果我們正在積累積壓的工作,我們可以增加更多的工作線程,這樣就可以很容易地擴大規模。

首先,讓我們嘗試同時運行兩個worker實例。它們都將從隊列中獲取消息,但具體是如何獲得的呢?讓我們看看。

你需要打開三個控制檯。兩個將運行worker程序。這些控制檯將是我們的兩個消費者-C1和C2。

# shell 1
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# shell 2
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C

在第三個控制檯我們將發佈新的任務。啓動消費者後,您可以發佈一些消息:

# shell 3
java -cp $CP NewTask First message.
# => [x] Sent 'First message.'
java -cp $CP NewTask Second message..
# => [x] Sent 'Second message..'
java -cp $CP NewTask Third message...
# => [x] Sent 'Third message...'
java -cp $CP NewTask Fourth message....
# => [x] Sent 'Fourth message....'
java -cp $CP NewTask Fifth message.....
# => [x] Sent 'Fifth message.....'

讓我們看看投遞到worker的是什麼:

java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

默認情況下,RabbitMQ將按順序將每條消息發送到下一個消費者。平均每個消費者將收到相同數量的消息。這種分發消息的方式稱爲輪詢。可以用三個或更多的消費者試試這個。

消息確認

完成任務可能需要幾秒鐘。你可能會想知道,如果一個消費者開始一項長期任務卻只完成了一部分,會發生什麼。在我們當前的代碼中,一旦RabbitMQ向消費者傳遞了一條消息,它就會立即將其標記爲刪除。在這種情況下,如果您殺掉一個消費者,我們將丟失它正在處理的消息。我們還將丟失已發送給這個線程但尚未處理(接受)的所有消息。

但我們不想失去任何任務(消息)。如果一個消費者死了,我們希望把任務交給另一個消費者。

爲了確保消息不會丟失,RabbitMQ支持消息確認。使用者會發回一個確認,告訴RabbitMQ已經收到、處理了特定的消息,並且RabbitMQ可以隨意刪除它。

如果一個使用者在沒有發送ack的情況下死亡(其通道關閉、連接關閉或TCP連接丟失),RabbitMQ將認爲消息未被完全處理,並將其重新放入隊列。如果有其他消費者同時在線,它會很快將其重新發送給另一個消費者。這樣你就可以確保沒有信息丟失,即使消費者偶爾死亡。

不會有任何消息超時;RabbitMQ將在消費者死亡時重新傳遞消息。即使處理一條消息需要很長時間也沒關係。

默認情況下,手動消息確認處於啓用狀態。在前面的例子中,我們通過autoAck=true標誌顯式地關閉了它們。一旦我們完成了一個任務,現在是時候將這個標誌設置爲false並從消費者那裏發送一個正確的確認。

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

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
  String message = new String(delivery.getBody(), "UTF-8");

  System.out.println(" [x] Received '" + message + "'");
  try {
    doWork(message);
  } finally {
    System.out.println(" [x] Done");
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
  }
};
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

使用這段代碼,我們可以確保即使您在處理消息時使用CTRL+C殺死一個worker(消費者),也不會丟失任何內容。在worker死後不久,所有未確認的消息都將被重新發送。

確認必須在接收傳遞的同一通道上發送。嘗試使用不同的通道進行確認將導致通道級協議異常。有關更多信息,請參閱文檔確認指南。

忘記確認
忘記basicAck是一個常見的錯誤。這是一個簡單的錯誤,但後果是嚴重的。當您的消費者客戶端退出時,消息將被重新傳遞(這看起來像隨機重新傳遞),但是RabbitMQ將消耗越來越多的內存,因爲它無法釋放任何未確認的消息。
爲了調試這種錯誤,可以使用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不允許您使用不同的參數重新定義現有隊列,並將向任何嘗試重新定義的程序返回錯誤。但是有一個快速的解決方法-讓我們聲明一個具有不同名稱的隊列,例如task_queue:

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

此queueDeclare更改需要同時應用於生產者代碼和使用者代碼。
這樣我們確保即使RabbitMQ重新啓動,任務隊列隊列也不會丟失。現在我們需要將消息標記爲persistent持久化的,方法是將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。

公平分發

你可能已經注意到調度仍然不能完全按照我們的要求工作。例如,在一個有兩個消費者的情況下,當所有的奇數消息都是重消息而偶數消息都是輕消息時,一個消費者將持續忙碌,而另一個消費者幾乎不做任何工作。嗯,RabbitMQ對此一無所知,仍然會均勻地發送消息。

這是因爲RabbitMQ只是在消息進入隊列時發送消息。它不考慮消費者未確認消息的數量。它只是盲目地將第n條消息發送給第n個消費者。
在這裏插入圖片描述
爲了避免這種情況,我們可以使用basicQos方法,並設置prefetchCount=1。這告訴RabbitMQ不要一次向一個消費者發出多個消息。或者,換句話說,在處理並確認前一條消息之前,不要向消費者發送新消息。相反,它將把它發送給下一個不忙的消費者。

int prefetchCount = 1;
channel.basicQos(prefetchCount);

關於隊列大小的說明
如果所有的消費者都很忙,你的隊列可能會滿的。你會想繼續關注這一點,也許會增加更多的消費者,或者有一些其他的策略。

把它們放在一起

我們的最終代碼NewTask.java類:

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;

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("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

        String message = String.join(" ", argv);

        channel.basicPublish("", TASK_QUEUE_NAME,
                MessageProperties.PERSISTENT_TEXT_PLAIN,
                message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + message + "'");
    }
  }

}

消費者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

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

    DeliverCallback deliverCallback = (consumerTag, delivery) -> {
        String message = new String(delivery.getBody(), "UTF-8");

        System.out.println(" [x] Received '" + message + "'");
        try {
            doWork(message);
        } finally {
            System.out.println(" [x] Done");
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    };
    channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
  }

  private static void doWork(String task) {
    for (char ch : task.toCharArray()) {
        if (ch == '.') {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException _ignored) {
                Thread.currentThread().interrupt();
            }
        }
    }
  }
}

可以使用消息確認和預取數設置工作隊列。即使RabbitMQ重新啓動,持久性選項也可以讓任務繼續存在。

有關通道方法和消息屬性的更多信息,您可以在線瀏覽JavaDocs

現在我們可以繼續學習教程3,學習如何向許多消費者傳遞同樣的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章