rabbitMQ學習筆記(3):Work Queues


在上一篇文章中我們解決了最簡單的helloworld 消息傳遞,這一篇中我們來探討rabbitMQ中的任務分發


rabbitMQ任務分發機制的核心出發點就是避免立刻進行“資源密集”或者說time-consuming的任務,因爲這樣就必須同步等待耗時任務的完成。取而代之的是schedule這些任務再稍後完成,在本篇的demo中我們將task封裝成一條message將其發送到隊列中。一個後臺運行的worker進程會從隊列中獲取message並執行任務。

任務分發機制在web應用中非常有用,因爲通常我們不會在一次http請求響應過程中處理複雜的耗時任務。  當有Consumer需要大量的運算時,RabbitMQ Server需要一定的分發機制來balance每個Consumer的load。

rabbitMQ的任務分發機制模型如下圖所示:


準備

在上一篇文章中的實例中,我們發送一個“hello world”的消息,在這篇文章中,我們發送一個字符串代表複雜的任務,用thread.sleep()函數模擬可能的操作,比如圖片的resize,pdf的內容渲染或者提取。

複用上文中的code,爲了便於區別,我們還是命名爲new_task.java

 String[] messages = {"a","b","c","d"};
	    String message = getMessage(messages);
	    //the concept of channel in rabbitMQ,the first parameter defines the name of exchange,
	    //"" means the default exchange
	    channel.basicPublish("", TASK_QUEUE_NAME,
	        MessageProperties.PERSISTENT_TEXT_PLAIN,
	        message.getBytes("UTF-8"));
	    System.out.println(" [x] Sent '" + message + "'");

getMessage方法,非常簡單:

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

原來的receiver代碼也需要略作改動,同樣爲了便於理解,我們將其重新命名爲worker.java,並且根據message中的“.”進行任務處理的模擬。

final Consumer consumer  = new DefaultConsumer(channel) {
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
					throws IOException {
				// TODO Auto-generated method stub
				//super.handleDelivery(consumerTag, envelope, properties, body);
				String message = new String(body,"UTF-8");
				
				try {
					doWork(message);

doWork方法:

private static void doWork(String task){
		for(char c : task.toCharArray()){
			System.out.print(c + "\t");
			if(c == '.'){
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						Thread.currentThread().interrupt();
					}
				
			}
		}
	}

至此我們已經完成了工作的大半。


round-robin dispatching 循環分發

RabbitMQ的分發機制非常適合擴展,而且它是專門爲併發程序設計的。如果現在load加重,那麼只需要創建更多的Consumer來進行任務處理即可。首先我們來運行兩個worker實例,這裏通過命令行的方式完成:

shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
 [*] Waiting for messages. To exit press CTRL+C

shell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
 [*] Waiting for messages. To exit press CTRL+C
然後producer將要發佈新任務:

shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask First message.
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Second message..
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Third message...
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fourth message....
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fifth message.....

我們來觀察一下 worker收到的消息

hell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Second message..'
 [x] Received 'Fourth message....'

默認情況下,rabbitMQ會按順序的將message依次分發給下一個consumer,這種分發方式就叫做round-robin。


Message Acknowledgement 消息確認

運行一個任務可能需要好幾秒甚至更久,那麼有個問題值得探究,如果一個consumer開始了一段長任務,但是在任務處理到一半時consumer進程異常退出會發生什麼。不幸的是,如果我們採用no-ack的方式,這個消息就消失了。也就是說,也就是說,每次Consumer接到數據後,而不管是否處理完成,RabbitMQ Server會立即把這個Message標記爲完成,然後從queue中刪除了。

如果一個Consumer異常退出了,它處理的數據能夠被另外的Consumer處理,這樣數據在這種情況下就不會丟失了(注意是這種情況下)。
      爲了保證數據不被丟失,RabbitMQ支持消息確認機制,即acknowledgments。爲了保證數據能被正確處理而不僅僅是被Consumer收到,那麼我們不能採用no-ack。而應該是在處理完數據後發送ack。

    在處理數據後發送的ack,就是告訴RabbitMQ數據已經被接收,處理完成,RabbitMQ可以去安全的刪除它了。
    如果Consumer退出了但是沒有發送ack,那麼RabbitMQ就會把這個Message發送到下一個Consumer。這樣就保證了在Consumer異常退出的情況下數據也不會丟失。

    這裏並沒有用到超時機制。RabbitMQ僅僅通過Consumer的連接中斷來確認該Message並沒有被正確處理。也就是說,RabbitMQ給了Consumer足夠長的時間來做數據處理。

message ack 默認情況下是開啓的,在上一節中我們通過autoAck=true來顯式的關閉了acknowledgement,現在我們修改handleDelivery回調函數,來發送確認信息。

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

Message Durability 消息持久化

上文中我們學習了在consumer異常退出或者中斷的情況下如何通過消息確認來保證消息的不丟失,但是在rabbitMQ server異常退出或者中斷情況下就無能爲力了,這種情況持久化消息可以幫忙。消息持久化需要做兩件事情就是聲明queue和message都是durable的:

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

上述語句執行不會有什麼錯誤,但是確得不到我們想要的結果,原因就是RabbitMQ Server已經維護了一個叫hello的queue,那麼上述執行不會有任何的作用,也就是hello的任何屬性都不會被影響。這一點在上篇文章也討論過。
那麼workaround也很簡單,聲明一個另外的名字的queue,比如名字定位task_queue:

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

再次強調,Producer和Consumer都應該去創建這個queue,儘管只有一個地方的創建是真正起作用的。
接下來,需要持久化Message,即在Publish的時候指定一個properties,方式如下:
mport com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue", 
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

Fair Dispatch 公平分發

你可能也注意到了,分發機制不是那麼優雅。默認狀態下,RabbitMQ將第n個Message分發給第n個Consumer。當然n是取餘後的。它不管Consumer是否還有unacked Message,只是按照這個默認機制進行分發。
   那麼如果有個Consumer工作比較重,那麼就會導致有的Consumer基本沒事可做,有的Consumer卻是毫無休息的機會。那麼,RabbitMQ是如何處理這種問題呢?


過 basic.qos 方法設置prefetch_count=1 。這樣RabbitMQ就會使得每個Consumer在同一個時間點最多處理一個Message。換句話說,在接收到該Consumer的ack前,他它不會將新的Message分發給它。 設置方法如下:

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

整合後的整個代碼如下:

new task.java

package cn.edu.nju.liushao.worker;

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";
	  private static final String MQ_ADDRESS = "localhost";
	  public static void main(String[] argv) throws Exception {
		  /*
		   * init factory,connection and channel 
		   */
	    ConnectionFactory factory = new ConnectionFactory();
	    factory.setHost(MQ_ADDRESS);
	    Connection connection = factory.newConnection();
	    Channel channel = connection.createChannel();
	    //declare a queue
	    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

	    String[] messages = {"a","b","c","d"};
	    String message = getMessage(messages);
	    //the concept of channel in rabbitMQ,the first parameter defines the name of exchange,
	    //"" means the default exchange
	    channel.basicPublish("", TASK_QUEUE_NAME,
	        MessageProperties.PERSISTENT_TEXT_PLAIN,
	        message.getBytes("UTF-8"));
	    System.out.println(" [x] Sent '" + message + "'");

	    channel.close();
	    connection.close();
	  }

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

worker.java

package cn.edu.nju.liushao.worker;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.AMQP.BasicProperties;

public class Worker {
	private static final String TASK_QUEUE_NAME = "task_queue";
	private static final String MQ_ADDRESS = "localhost";
	
	public static void main(String[] args) throws IOException, TimeoutException {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(MQ_ADDRESS);
		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, BasicProperties properties, byte[] body)
					throws IOException {
				// TODO Auto-generated method stub
				//super.handleDelivery(consumerTag, envelope, properties, body);
				String message = new String(body,"UTF-8");
				
				try {
					doWork(message);
				} finally {
					System.out.println("[x] done");
					// send back acknowledgement
					channel.basicAck(envelope.getDeliveryTag(), false);
				}
				
				
			}
		};
		
		channel.basicConsume(TASK_QUEUE_NAME, false,consumer);
	}
	
	private static void doWork(String task){
		for(char c : task.toCharArray()){
			System.out.print(c + "\t");
			if(c == '.'){
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						Thread.currentThread().interrupt();
					}
			}
		}
	}
}


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