消息隊列研究之RabbitMQ任務分發篇
在上一篇文章《消息隊列研究之RabbitMQ入門篇》 中,介紹了關於RabbitMQ入門的第一個程序Hello World!,該程序演示了在RabbitMQ中,單個發送者和單個接收者的使用,而實際使用中,多線程環境是最爲常見的,也就是同時有多個消息訂閱者同時訂閱,而發送者也可以同時有多個,而本篇文章就介紹下在多線程訂閱時,該如何使用及注意的事項。
l 例子描述
l 輪循分發
l 手動應答
l 持久化
l 轉發數
1、例子描述
本篇的例子很簡單:首先,註冊三個消息的接收者,然後,創建一個消息的發送者,發送10條消息,註冊的三個消息接受者,會區域平均分配接收這10條消息,因爲是10條消息,不能被三個接受者完全平分,所以RabbitMQ會隨機從三者中選出一個比較空閒的一個接受者,給於其發送4條消息,其它兩者各發送3條消息(如果發送的是9條消息,那麼每次發送時,三個接受者都會接收3條消息,這也是RabbitMQ輪循發送機制,其不會考慮哪個線程目前的忙閒狀態,只是平均分發)。
2、輪循分發
接收者:
publicclass ReceiverHandlerextendsBaseConnector implements Runnable,Consumer {
privateMessageInfo messageInfo =new MessageInfo();
privateint hashCode;
privatevolatile Thread thread;
publicReceiverHandler(String queueName)throws IOException, TimeoutException {
super(queueName);
}
publicvoid receiveMessage() {
Stringop_result = channel.basicConsume(queueName, true,this); //指定消費隊列
if("".equals(op_result)){
System.out.println("BasicConsumeConfig Consumer Queue Error!");
}
}catch (IOException e) {
System.out.println("Consumer Delivery Error,Msg info:" + e.getMessage());
}catch (Exception e) {
System.out.println("ErrorIs Opening,Msg info:" + e.getMessage());
}finally {
//TODO
}
}
@Override
publicvoid handleCancel(String arg0)throws IOException {
}
@Override
publicvoid handleCancelOk(String arg0) {
}
@Override
publicvoid handleConsumeOk(String consumeOk) {
}
@Override
publicvoid handleDelivery(String consumerTag, Envelope env,
BasicPropertiesprops,byte[] body)throws IOException {
messageInfo= (MessageInfo) SerializationUtils.deserialize(body);
System.out.println(msgInfo.getHashCode()+ " [REC] Received '" + messageInfo.getContent() + "'");
}
@Override
publicvoid handleRecoverOk(String arg0) {
}
@Override
publicvoid handleShutdownSignal(String arg0, ShutdownSignalException arg1) {
}
@Override
publicvoid run() {
receiveMessage();
}
publicvoid start() {
if(thread == null){
thread = new Thread(this);
thread.start();
}
}
}
發送者:
publicclass PublisherHandlerextendsBaseConnector {
publicPublisherHandler(String queueName)throws IOException,TimeoutException {
super(queueName);
}
publicvoid sendMessage(MessageInfo messageInfo) {
channel.basicPublish("",queueName, null, SerializationUtils.serialize(messageInfo));
}
publicvoid close() {
super.close();
}
}
測試入口:
public static void main(String[] args) {
PublisherHandler publisher = null;
ReceiverHandlerreceiver = null;
ReceiverHandlerreceiver2 = null;
ReceiverHandlerreceiver3 = null;
private String queueName="mq_demo" ;
try{
receiver= new ReceiverHandler(queueName); //接收者1
receiver.start();
receiver2= new ReceiverHandler(queueName); //接收者2
receiver2.start();
receiver3= new ReceiverHandler(queueName); //接收者3
receiver3.start();
publisher= newPublisherHandler(queueName); //發送者
for(inti=0;i<10;i++) {
String message = "消息【"+(i+1)+"】";
MessageInfomsgInfo =new MessageInfo();
msgInfo.setConsumerTag("demo_tag");
msgInfo.setChannel("demo");
msgInfo.setContent(message);
publisher.sendMessage(msgInfo);
}
}catch (IOException | TimeoutException e) {
e.printStackTrace();
}finally {
publisher.close();
}
}
結果顯示:
通過線程的hashcode匹對,上圖已經證實了最開始例子描述的需求,那麼請繼續往下閱覽!
3、手動應答
會有這樣一種情況存在,某一個或多個消息接受者在接收消息過程中,突然被停止掉或是不能正常工作時,不論該消息接受者是否接收到消息,RabbitMQ都會自動刪除它認爲已經發送出去的消息,這樣就造成接受者接收消息失敗數據丟失問題,那麼RabbitMQ自然會考慮到,所以我們需要在消息接受者接到消息之後,給予RabbitMQ服務反饋一個應答,告訴其我已經成功收到消息了,你可以刪除了,這樣就可以解決掉多線程時,訂閱者突然掛掉,不能接收消息丟失數據的問題了。
接收者改動:
在接收消息時,開啓應答模式,並在收到消息後,手動發送應答消息反饋給RabbitMQ消息服務,具體如下:
開啓應答模式:
// ……
// 指定消費隊列並開啓應答模式(第二個參數爲false,代表開啓應答,否則反之)
String op_result= channel.basicConsume(queueName, false,this);
// ……
手動反饋應答:
@Override
publicvoid handleDelivery(StringconsumerTag, Envelope env,
BasicPropertiesprops,byte[] body)throws IOException {
//應答模式下每發完消息後手動發送應答
channel.basicAck(env.getDeliveryTag(),false);
// ……
}
4、持久化
也會有這樣的情況存在,就是RabbitMQ服務掛掉,這時也會造成數據丟失,並且有時,我們也需要保留髮送的消息數據備用,那麼這時我們應該怎麼處理?答案是,RabbitMQ也爲你考慮好,就是設置數據的持久化。RabbitMQ的數據持久化分爲隊列和消息內容持久化兩種,分別如下操作:
隊列持久化:
此時,我們需要修改優化下BaseConnector隊列聲明部分,添加如下:
// 聲明創建隊列並設置隊列持久化(第二個參數爲true,代表該隊列持久化反之)
channel.queueDeclare(queueName,true, false,false,null);
消息持久化:
此時,我們需要在發送消息時設置,修改優化下sendMessage(…)方法,具體如下:
// ……
// 消息發送及聲明消息持久化bp
BasicProperties bp = MessageProperties.PERSISTENT_TEXT_PLAIN;
channel.basicPublish("",queueName,bp,SerializationUtils.serialize(messageInfo));
5、轉發數
當訂閱者不能正常工作時,RabbitMQ會將餘下的消息發送給可以工作的接收訂閱者進程繼續接收處理消息,但是默認下,RabbitMQ不會評估消息接受者當前的忙閒狀態,依舊按部就班的逐個轉發一個或是多個消息給到訂閱者,這樣一旦某個訂閱者當前較忙,可依舊收到過多的消息,而其它訂閱者比較閒時,就造成了系統瓶頸壓力問題,所以我們需要告訴RabbitMQ服務,每次最多轉發多少消息,給到目前較空閒的訂閱者,那麼需要調整訂閱者的receiveMessage方法,添加如下內容:
// ……
// 消息最大轉發數默認爲1
channel.basicQos(1);
// ……
最後,我們給出同時滿足應答響應、隊列/消息持久化及轉發數設定的訂閱者、發送者及基類的完整代碼:
繼承的基類:
publicclassBaseConnector {
protectedChannel channel;
protectedConnection connection;
protectedString queueName;
publicBaseConnector(String queueName,boolean isQueueDurable)throwsIOException, TimeoutException {
this. queueName = queueName;
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
connection = factory.newConnection();
channel = connection.createChannel();
// 聲明創建隊列並隊列持久化
channel.queueDeclare(queueName,isQueueDurable,false,false,null);
}
protectedvoid close() {
try {
channel.close();
connection.close();
}catch (IOException e) {
e.printStackTrace();
}catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消息接收者:
publicclass ReceiverHandlerextendsBaseConnector implements Runnable,Consumer {
privateMessageInfo messageInfo =new MessageInfo();
privateint hashCode;
privatevolatile Thread thread;
publicReceiverHandler(String queueName, boolean isQueueDurable)throwsIOException, TimeoutException {
super(queueName,isQueueDurable);
}
publicvoid receiveMessage() {
Stringop_result = channel.basicConsume(queueName, false,this); //指定消費隊列並開啓應答
if("".equals(op_result)){
System.out.println("BasicConsumeConfig Consumer Queue Error!");
}
}catch (IOException e) {
System.out.println("Consumer Delivery Error,Msg info:" + e.getMessage());
}catch (Exception e) {
System.out.println("ErrorIs Opening,Msg info:" + e.getMessage());
}finally {
//TODO
}
}
@Override
publicvoid handleCancel(String arg0)throws IOException {
}
@Override
publicvoid handleCancelOk(String arg0) {
}
@Override
publicvoid handleConsumeOk(String consumeOk) {
}
@Override
publicvoid handleDelivery(String consumerTag, Envelope env,
BasicPropertiesprops,byte[] body)throws IOException {
//應答模式下每發完消息後手動發送應答
channel.basicAck(env.getDeliveryTag(),false);
messageInfo= (MessageInfo) SerializationUtils.deserialize(body);
System.out.println(msgInfo.getHashCode()+ " [REC] Received '" + messageInfo.getContent() + "'");
}
@Override
publicvoid handleRecoverOk(String arg0) {
}
@Override
publicvoid handleShutdownSignal(String arg0, ShutdownSignalException arg1) {
}
@Override
publicvoid run() {
receiveMessage();
}
publicvoid start() {
if(thread == null){
thread = new Thread(this);
thread.start();
}
}
}
消息發送者:
publicclass PublisherHandlerextendsBaseConnector {
publicPublisherHandler(String queueName, boolean isQueueDurable)throws IOException,TimeoutException {
super(queueName,isQueueDurable);
}
publicvoid sendMessage(MessageInfo messageInfo) {
// 消息發送及聲明消息持久化bp
BasicProperties bp = MessageProperties.PERSISTENT_TEXT_PLAIN;
channel.basicPublish("",queueName, bp, SerializationUtils.serialize(messageInfo));
}
publicvoid close() {
super.close();
}
}
說明:
隊列的類型是固定的,不能動態修改,也就是當隊列mq_demo一開始爲非持久化隊列,那麼開啓持久化後,會報錯,一般需要修改隊列名字爲新的名字。
消息隊列RabbitMQ任務分發就介紹到這裏,由於作者水平有限,如有問題請在評論發言或是QQ羣(245389109(新))討論,謝謝。