注:這是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
在第一篇指導(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è)人。
為了解決這個(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é)文章,歡迎討教。
--謝謝--