2.Work Queues#前山翻譯

注:這是RabbitMQ-java版Client的指導(dǎo)教程翻譯系列文章,歡迎大家批評(píng)指正
第一篇Hello Word了解RabbitMQ的基本用法
第二篇Work Queues介紹隊(duì)列的使用
第三篇Publish/Subscribe介紹轉(zhuǎn)換器以及其中fanout類(lèi)型
第四篇Routing介紹direct類(lèi)型轉(zhuǎn)換器
第五篇Topics介紹topic類(lèi)型轉(zhuǎn)換器
第六篇RPC介紹遠(yuǎn)程調(diào)用

Work Queues

java-two.png

在第一篇指導(dǎo)教程中,我們寫(xiě)到應(yīng)用去發(fā)送消息到隊(duì)列中和從隊(duì)列中取出消息。在這篇教程中將會(huì)創(chuàng)建一個(gè)工作隊(duì)列,用于在多個(gè)工作者間按效率分配任務(wù)。

工作隊(duì)列的主要思想是避免馬上去做一件消耗資源的任務(wù),而可以等著它完成。換句話說(shuō)分配任務(wù)可以稍后去完成。我們把一個(gè)任務(wù)當(dāng)作一個(gè)消息,然后發(fā)送到一個(gè)隊(duì)列中。一個(gè)后臺(tái)執(zhí)行的工作進(jìn)程就可以取出任務(wù),并且最終執(zhí)行這項(xiàng)任務(wù)。當(dāng)你運(yùn)行多個(gè)工作者的時(shí)候,許多工作就可以在這些工作者之間共享完成。

這個(gè)概念在web應(yīng)用中尤其有用,web應(yīng)用一般是在簡(jiǎn)短的http請(qǐng)求中去處理復(fù)雜的任務(wù)。

準(zhǔn)備工作(Preparation)

在上篇指導(dǎo)教程中我們發(fā)送一條包含字符串Hello World的信息,現(xiàn)在我們將發(fā)送代表復(fù)雜任務(wù)的字符串。我們并沒(méi)有真正的工作任務(wù),像圖片的壓縮,或者渲染pdf文件。因此我們通過(guò)假裝很忙去實(shí)現(xiàn)它-使用線程睡眠的功能。我們將字符串中每個(gè)小點(diǎn)都當(dāng)作一個(gè)復(fù)雜的任務(wù),每一個(gè)小點(diǎn)都需要一秒的工作時(shí)間。例如,一個(gè)偽裝的項(xiàng)目為“Hello...”需要三秒來(lái)完成。

我們將對(duì)以前例子中的Send.java類(lèi)中稍作修改,允許通過(guò)命令行發(fā)送任意的消息。這個(gè)程序?qū)?huì)分配任務(wù)到我們的工作隊(duì)列中,因此命名為NewTask.java:

String message = getMessage(argv);

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

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

從命令行的參數(shù)中獲取到消息:

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也需要做一些修改:它需要在消息中對(duì)每一個(gè)小點(diǎn)做一個(gè)一秒的消耗,將處理被分發(fā)到的的消息并最終完成任務(wù)。因此命名為Work.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消息的應(yīng)答機(jī)制

channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);  //接受消息的監(jiān)聽(tīng)

捏造的任務(wù)需要計(jì)算時(shí)間:

private static void doWork(String task) throws InterruptedException {

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

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

    }

}

編譯它們就像在上一篇指導(dǎo)教程中一樣

javac -cp $CP NewTask.java Worker.java

循環(huán)分發(fā)機(jī)制(Round-robin dispatching)

創(chuàng)建一個(gè)隊(duì)列的優(yōu)勢(shì)之一就是可以方面的同時(shí)做各個(gè)任務(wù),如果我們需要處理大量擠壓的工作,只需要增加更多工作者就可以解決。

第一步同時(shí)運(yùn)行兩個(gè)工作者應(yīng)用,它們將都會(huì)從隊(duì)列中獲取到消息。但是如何獲取呢,我們拭目以待。

需要把三個(gè)控制臺(tái)打開(kāi),兩個(gè)運(yùn)行工作者應(yīng)用,也就是我們的兩個(gè)消費(fèi)者: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

第三個(gè)用來(lái)發(fā)布新的任務(wù),只要先創(chuàng)建好了消費(fèi)者,你就發(fā)布一些消息:

# shell 3

java -cp $CP NewTask

# => First message.

java -cp $CP NewTask

# => Second message..

java -cp $CP NewTask

# => Third message...

java -cp $CP NewTask

# => Fourth message....

java -cp $CP NewTask

# => Fifth message.....

我們來(lái)看看是如何分發(fā)給工作者的:

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....'

以上可見(jiàn),RabbitMQ默認(rèn)的方式是有序的發(fā)送消息給每一個(gè)消費(fèi)者,一般每個(gè)消費(fèi)者都會(huì)持有相同數(shù)目的消息。這種分發(fā)消息的方式叫做循環(huán)分發(fā)機(jī)制(round-robin)。可以嘗試三個(gè)或者更多的工作者。

消息應(yīng)答機(jī)制(Message acknowledgment)

完成一項(xiàng)任務(wù)需要花費(fèi)一些時(shí)間。你可能會(huì)想:當(dāng)一個(gè)消費(fèi)者處理一個(gè)比較耗時(shí)的任務(wù)時(shí),只完成一部分就死掉了會(huì)發(fā)生什么事情?按照我們目前的代碼來(lái)看,RabbitMQ分發(fā)消息給消費(fèi)者后就立刻從內(nèi)存中移除該消息。這種情境中,如果你殺死這個(gè)消息者,被消費(fèi)者正處理的消息就會(huì)被丟失。由此可見(jiàn),這樣分發(fā)的所有消息還沒(méi)有被消費(fèi)者處理就全部丟失了。

但是我們并不像丟失任何的任務(wù),如果一個(gè)工作者死了,我們想把這個(gè)任務(wù)轉(zhuǎn)發(fā)給其他的工作者。

為了確保這個(gè)消息沒(méi)有被丟失,RabbitMQ支持消息應(yīng)答機(jī)制。消息應(yīng)答機(jī)制是當(dāng)消費(fèi)者接受到消息后可以返回一條信息告訴RabbitMQ這條消息已經(jīng)被接受并處理了,然后RabbitMQ就可以刪除該消息。

如果一個(gè)消費(fèi)者死了(通道關(guān)閉,連接關(guān)閉,或者TCP連接失敗),沒(méi)有發(fā)送一個(gè)應(yīng)答反饋,RabbitMQ將會(huì)理解為這個(gè)消息還沒(méi)有被處理,那么消息還將存在消息隊(duì)列中。如果同時(shí)還有其它的消費(fèi)者存活著,這個(gè)消息將會(huì)被重新發(fā)送給其它的消費(fèi)者。這種方式就可以確保沒(méi)有消息丟失,即使消費(fèi)者意外死了。

沒(méi)有處理消息超時(shí)的情況,除非這個(gè)消費(fèi)者死了RabbitMQ才會(huì)重新分發(fā)消息。如果處理一個(gè)消息要花費(fèi)很長(zhǎng)很長(zhǎng)的時(shí)間,RabbitMQ也會(huì)等待。

消息應(yīng)答機(jī)制默認(rèn)是開(kāi)啟的,但是在上一篇指導(dǎo)教程中是通過(guò)設(shè)置autoAck=true標(biāo)識(shí)將他們關(guān)閉的。現(xiàn)在將這個(gè)標(biāo)識(shí)設(shè)置為false,并且工作者在完成任務(wù)后,發(fā)送一條合適的應(yīng)答反饋。

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

我們相信:使用這種方法,即使通過(guò)CTRL+C殺死一個(gè)正在處理消息的工作者,也沒(méi)有消息會(huì)丟失。在這個(gè)工作者死了之后,沒(méi)有應(yīng)答反饋的消息將會(huì)被重新分發(fā)。

忘記了消息應(yīng)答反饋

這是一個(gè)常見(jiàn)的錯(cuò)誤:沒(méi)有調(diào)用basicAck方法。也是很容易犯的錯(cuò)誤,但是結(jié)果很?chē)?yán)重。當(dāng)消費(fèi)者放棄時(shí),消息將會(huì)被重新分發(fā)(看起來(lái)會(huì)很少分發(fā))。但是RabbitMQ將會(huì)消耗大量的內(nèi)存,因?yàn)樗荒軌蜥尫湃魏挝磻?yīng)答反饋的消息。

為了調(diào)試這類(lèi)型的錯(cuò)誤,可以使用rabbitmqctl去打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

在window上,不需要sudo:

rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged

消息的持久性(Message durability)

我們已經(jīng)知道如何確保在消費(fèi)者死了的情況下,這個(gè)任務(wù)還不會(huì)丟失。但是在RabbitMQ服務(wù)端停止的時(shí)候,這個(gè)任務(wù)還是會(huì)丟失。

當(dāng)RabbitMQ服務(wù)端停止或者崩潰的時(shí)候,它的所有隊(duì)列和消息都會(huì)丟失,除非你告訴它兩件事才能確保消息不會(huì)被丟失:標(biāo)記隊(duì)列和消息持久化。

第一步,我們確保RabbitMQ不會(huì)丟失隊(duì)列,為了達(dá)到這個(gè)目的,需要將它聲明持久化:

boolean durable = true;

channel.queueDeclare("hello", durable, false, false, null);

盡管這行代碼本身是正確的,但是對(duì)于已經(jīng)存在的隊(duì)列再次創(chuàng)建是不會(huì)有效的,因?yàn)槲覀円呀?jīng)定義了一個(gè)不是持久化隊(duì)列的名字叫Hello。RabbitMQ不會(huì)容許你重新定義一個(gè)帶有不同參數(shù)的已經(jīng)存在的隊(duì)列,任何應(yīng)用這么做的話,它將會(huì)返回一個(gè)錯(cuò)誤。但是這里有變通方案:聲明一個(gè)不一樣的隊(duì)列名字。例如:task_queue:

boolean durable = true;

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

這個(gè)變化的方法需要同時(shí)應(yīng)用在生產(chǎn)者和消費(fèi)者的代碼中。

現(xiàn)在我們可以確定,即使RabbitMQ重新啟動(dòng),task_queue隊(duì)列都不會(huì)丟失。接著我們需要標(biāo)記消息持久化,通過(guò)設(shè)置MessageProperties(它實(shí)現(xiàn)了BasicProperties接口)的值為PERSITENT_TEXT_PLAIN.

import com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue",

MessageProperties.PERSISTENT_TEXT_PLAIN,

message.getBytes());

注意:消息持久化

標(biāo)記消息持久化并不能完全保證消息一定不會(huì)丟失。雖然已經(jīng)告訴RabbitMQ需要持久化消息到硬盤(pán)上,但還有一小段時(shí)間,當(dāng)RabbitMQ已經(jīng)接受到消息但還沒(méi)有保存的時(shí)候,或者RabbitMQ沒(méi)有為每個(gè)消息異步操作時(shí),就有可能值保存在緩存中而沒(méi)有寫(xiě)入到磁盤(pán)上。這個(gè)持久化的保證并不強(qiáng)大,但是已經(jīng)足夠我們簡(jiǎn)單任務(wù)隊(duì)列的使用。如果你需要更強(qiáng)大的保證,你可以使用發(fā)布確認(rèn)機(jī)制

高效分發(fā)(Fair dispatch)

你可能已經(jīng)注意到這樣的分發(fā)方式仍不是我們想的那樣。舉例來(lái)說(shuō),一個(gè)場(chǎng)景中有兩個(gè)工作者,當(dāng)有些消息是復(fù)雜的,有些消息是簡(jiǎn)單的,一個(gè)工作者可能會(huì)一直很忙,而另外一個(gè)工作者幾乎不用做什么工作。RabbitMQ并不知道它們的工作情況,并且仍會(huì)循環(huán)的分發(fā)消息。

當(dāng)消息進(jìn)入隊(duì)列中,RabbitMQ僅僅只是分發(fā)消息,它并不會(huì)查看消費(fèi)者的反饋信息,只是盲目的分發(fā)第N個(gè)消息給第N個(gè)人。


prefetch-count.png

為了解決這個(gè)問(wèn)題,我們可以使用basicQos方法,設(shè)置prefetchCount屬性值為1。這個(gè)表明在一個(gè)時(shí)間點(diǎn)RabbitMQ不要分發(fā)更多的消息給工作者,或者換句話說(shuō),直到上個(gè)消息被處理并且得到應(yīng)答反饋,才能分發(fā)一條新的消息給這個(gè)工作者。或者RabbitMQ會(huì)分發(fā)給一個(gè)工作不忙的工作者這個(gè)新消息。

int prefetchCount = 1;

channel.basicQos(prefetchCount);

注意隊(duì)列的大小

如果所有的工作者都很忙,隊(duì)列可能溢出。你需要注意這方面問(wèn)題,如果存在這個(gè)問(wèn)題可以添加更多的工作者,或者使用其他的一些策略方式。

綜合

最終NewTask.java類(lèi)的代碼,這里下載

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

    }

   //...

}

以及Work.java代碼,這里下載

import com.rabbitmq.client.*;

import java.io.IOException;

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

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

}

    private static void doWork(String task) {

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

            if (ch == '.') {

                try {

                   Thread.sleep(1000);

                } catch (InterruptedException _ignored) {

                    Thread.currentThread().interrupt();

}}}}}

在創(chuàng)建工作隊(duì)列時(shí),使用消息應(yīng)答機(jī)制和basicBos的參數(shù)值設(shè)置;即使RabbitMQ服務(wù)端重啟,通過(guò)設(shè)置持久化的一些選項(xiàng)就可以保存任務(wù)。
想了解更多相關(guān)Channel的信息和MessageProperties屬性,可以點(diǎn)擊這里查看

第二節(jié)的內(nèi)容大致翻譯完了,這里是原文鏈接。接著進(jìn)入下一節(jié):Publish/Subscribe

終篇是我對(duì)RabbitMQ使用理解的總結(jié)文章,歡迎討教。
--謝謝--

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 來(lái)源 RabbitMQ是用Erlang實(shí)現(xiàn)的一個(gè)高并發(fā)高可靠AMQP消息隊(duì)列服務(wù)器。支持消息的持久化、事務(wù)、擁塞控...
    jiangmo閱讀 10,408評(píng)論 2 34
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,973評(píng)論 19 139
  • RabbitMQ詳解 本文地址:http://www.host900.com/index.php/articles...
    嘉加家佳七閱讀 2,548評(píng)論 0 9
  • 1.引言 RabbitMQ——Rabbit Message Queue的簡(jiǎn)寫(xiě),但不能僅僅理解其為消息隊(duì)列,消息代理...
    圣杰閱讀 2,130評(píng)論 3 39
  • 工作隊(duì)列 在上一個(gè)教程中,我們寫(xiě)了一個(gè)從一個(gè)已經(jīng)命好名的隊(duì)列中收發(fā)消息的程序。在這個(gè)教程中,我們將創(chuàng)建一個(gè)工作隊(duì)列...
    番薯IT閱讀 982評(píng)論 0 5