RabbitMQ 之二 "Work Queues"

在上一篇RabbitMQ HelloWorld中,我们写了一个程序关于向一个queue 发送消息和从同一个queue中取出消息,这篇文章,我们将创建一个 Work Queue用来向多个worker分发耗时的tasks。

关于Work Queues的主要目的是为了避免立即执行资源密集型任务,并且必须等待它完成,相反,我们可以安排任务在稍后执行我们将一个task压缩为message,并且发送给queue,然后一个运行在后台的worker进程最终会获取这个task并执行,多个works进程可以共享这些tasks。

这个概念在web应用程序中尤其有用,因为在短HTTP链接中不能处理复杂的任务。这会导致糟糕的用户体验。

在上一篇文章中,Producer往queue中发送了一个"Hello World"的消息,因为没有真实的复杂task,在这篇文章中我们用字符串来代表复杂的tasks,在字符串中用.来代表task的复杂度,一个点代表需要消耗1s时间,在代码中我们可以通过Thread.sleep()来模拟任务的耗时,比如一个代表task的字符串Hello.....表示这个任务需要耗时5s。

代码只需要在上篇文章的基础上加以改造就可以了。消息模型为单发送多接收

RabbitMQ 之二

代码如下:

producer端NewTask.java

package org.rabbitmq.workqueue;

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");
	    factory.setPort(5673);
	    Connection connection = factory.newConnection();
	    Channel channel = connection.createChannel();

	    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

	    String message = getMessage(argv);
	    for(int i=0;i<=5;i++){
	    	message = "message"+i + ".....";
	    	channel.basicPublish("", TASK_QUEUE_NAME,
	    			MessageProperties.PERSISTENT_TEXT_PLAIN,
	    			message.getBytes("UTF-8"));
	    	System.out.println(" [x] Sent '" + message + "'"+i);
	    }

	    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();
	  }
}
Consumer端Worker.java

package org.rabbitmq.workqueue;


import java.io.IOException;


import com.rabbitmq.client.AMQP;
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;


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");
	    factory.setPort(5673);
	    final Connection connection = factory.newConnection();
	    final Channel channel = connection.createChannel();
	    
	    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
	    System.out.println("worker thread:"+Thread.currentThread().hashCode()+" [*] 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("[isredeliver]"+envelope.isRedeliver()+" [x] Done");
	          //是否确认消息已被消费
//	          channel.basicAck(envelope.getDeliveryTag(), false);
	        }
	      }
	    };
	    boolean autoAck = true;
	    //启动一个消费者进程 是否自动确认消息被消费
	    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();
	        }
	      }
	    }
	  }
}

Round-robin dispatching

关于task queue的有点就是可以并行工作,假设我们积压了大量的work,我们只需要添加worker就可以解决这个问题。
首先,在eclipse中执行两次work,这两个work都会从name为task_queue的queue中获取消息,再eclipse中执行NewTask
RabbitMQ 之二
RabbitMQ 之二

RabbitMQ 之二

在Producer端我们一共发送了6个消息,两个worker每个接收三个消息。
默认情况下,RabbitMQ会按顺序将消息一个一个发送给下一个消费者,每个消费者将会获取同等数量的消息,这种消息分发机制称之为round-robin。

Message acknowledgment消息确认机制

一个task可能需要花费数秒时间,当一个worker在执行耗时的task中发生死亡退出,会发生什么?在我们目前的代码中,当RabbitMQ分发消息给consumer后会立即将这个消息打上删除的标记,这种情况下,如果杀死正在执行work的工作者进程,那将会失去所有分发给这个worker但是还没有处理完成的消息。
我们并不希望丢失任何消息,当一个工作者进程挂掉之后,哦我们希望重新分发消息给其他工作者处理。
为了不让消息丢失,RabbitMQ 支持消息确认机制(Message acknowledgment),ack标志是由consumer回传给RabbitMQ用以说明特定消息被成功接收、处理。然后RabbitMQ就可以删掉该消息了。
当一个consumer挂掉但是没有回传消息的ack标志,这种情况下一旦有其他的consumers存活, RabbitMQ就会重新分发这个消息,这样就可以保证消息不丢失。
RabbitMQ中消息没有超时这一概念,只有在consumer挂掉之后,RabbitMQ才会重新分发消息。
默认情况下,消息默认机制是打开的,当然我们也可以手动进行消息确认。可以通过这个方法来开启手动消息确认机制
    /**
     * Start a non-nolocal, non-exclusive consumer, with
     * a server-generated consumerTag.
     * @param queue the name of the queue
     * @param autoAck true if the server should consider messages
     * acknowledged once delivered; false if the server should expect
     * explicit acknowledgements
     * @param callback an interface to the consumer object
     * @return the consumerTag generated by the server
     * @throws java.io.IOException if an error is encountered
     * @see com.rabbitmq.client.AMQP.Basic.Consume
     * @see com.rabbitmq.client.AMQP.Basic.ConsumeOk
     * @see #basicConsume(String, boolean, String, boolean, boolean, Map, Consumer)
     */
    String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
稍微改下代码再来看运行结果:
当设置autoAck=false时,运行结果如下
RabbitMQ 之二
RabbitMQ 之二
因为我们设置了channel.basicQos(1);,并且关闭了消息的自动确认,所以RabbitMQ在channel里面的消息确认ack之前,只会往里面放一条消息。
当设置channel.basicQos(3)后,channel里面的unacknowledged的消息数量为3,重新执行代码,结果如下
RabbitMQ 之二
RabbitMQ 之二
RabbitMQ的插件可以显示这六条消息都未被确认
RabbitMQ 之二
重新运行worker两次可以看到
RabbitMQ 之二
RabbitMQ 之二
redeliver标志为true说明这消息是重新转发的。
注意:别忘记消息确认
丢失basicAck是经常发生的错误,结果会导致消息不断被重新分发当consumer client挂掉的时候,这种情况下RabbitMQ会占用越来越多的内存,因为RabbitMQ不会释放未被确认的消息。
消息持久化
当客户端挂掉之后,RabbitMQ可以使用消息确认机制来确保消息不会丢失,但是当RabbitMQ server挂掉之后呢,应该如何保证消息不丢失?
RabbitMQ提供了持久化机制来确保RabbitMQ Server挂掉之后消息不丢失。
声明一个持久化的消息队列很简单
   /**
     * Declare a queue
     * @see com.rabbitmq.client.AMQP.Queue.Declare
     * @see com.rabbitmq.client.AMQP.Queue.DeclareOk
     * @param queue the name of the queue
     * @param durable true if we are declaring a durable queue (the queue will survive a server restart)
     * @param exclusive true if we are declaring an exclusive queue (restricted to this connection)
     * @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)
     * @param arguments other properties (construction arguments) for the queue
     * @return a declaration-confirm method to indicate the queue was successfully declared
     * @throws java.io.IOException if an error is encountered
     */
    Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                 Map arguments) throws IOException;
声明queue时设置durable为true时即可。
参考: http://www.rabbitmq.com/tutorials/tutorial-two-java.html

你可能感兴趣的:(RabbitMQ,java)