RabbitMQ之任务队列【译】

在第一个教程里面,我们写了一个程序从一个有名字的队列中发送和接收消息,在这里我们将要创建一个分发耗时任务给多个worker的任务队列。

![](http://images2015.cnblogs.com/blog/658141/201608/658141-20160817001132015-1165677723.png)

任务队列核心思想就是避免执行一个资源密集型的任务,而程序要等待其执行完毕才能进行下一步的任务。相反地我们让任务延迟执行,我们封装一个task作为消息,并把它发送至队列,在后台运行的工作进程将弹出的任务,并最终执行作业。当运行多个worker的时候,task将在他们之间共享。

准备

在前一节中我们发送一个包含“HelloWorld!”的消息,现在我们发送字符串代表一个复杂的任务,我们没有一个真实的任务,比如格式化图片大小等等,所以我们使用Thread.sleep()代表一个执行时间较长的任务,这里我们使用几个点来代表任务的复杂度,每一个点代表任务执行一秒的时间,比如hello...就代表执行了3秒。
我们稍微改变一个上一节中的Send.java,允许从命令行发送任意的消息,程序将从我们的工作队列中执行任务,所以命名为NewTask.java:

String message = getMessage(argv);

channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");

以下是帮助从命令行参数获取消息体的代码:

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

上一节中的Rece.java也需要少许改变:需要伪造一个根据点来执行多少秒的任务。它将处理传送过来的消息,并且执行任务,命名为Worker.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");
    }
  }
};
channel.basicConsume(TASK_QUEUE_NAME, true, consumer);

模拟执行时间的任务:

private static void doWork(String task) throws InterruptedException {
    for (char ch: task.toCharArray()) {
        if (ch == '.') Thread.sleep(1000);
    }
}

编译:

$ javac -cp rabbitmq-client.jar NewTask.java Worker.java

循环调度

使用任务队列的优点之一就是很容并行化一个work,如果我们产生了工作积压,我们可以很简单的增加worker的数量,来解决问题。
首先,让我们尝试在同一时间运行两个工人实例。他们都将在队列中得到消息,但究竟如何?让我们来看看。
您需要三个控制台打开。两个将运行辅助程序。这些控制台将是我们的两名消费者 - C1和C2。

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

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

在第三个,我们将发布新的任务。一旦你开始运行消费者就可以发布几条消息:

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的:

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
 [x] Received 'First message.'
 [x] Received 'Third message...'
 [x] Received 'Fifth message.....'
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
 [x] Received 'Second message..'
 [x] Received 'Fourth message....'

默认情况下,RabbitMQ将发送每一个在序列中的消息到下一个消费者,平均而言,每一个消费者将获得相同数量的消息,发布这种消息的方式叫循环调度。

消息确认

做一个任务需要几秒钟,那么当一个消费者执行任务到一半的时候挂了怎么办?在我们的当前代码里面,一旦消息传送给我们的消费者,消息就从存储中删除了。在这种情况下,如果Kill了一个worker,我们不仅仅失去了它正在执行的消息任务,而且我们将失去所有分配给它,但是还没执行的消任务。
但是我们不想丢失任何消息,如果一个worker挂掉,我们将分配这些任务给其他的消费者。
为了确保消息不会丢失,RabbitMQ支持消息确认。一个ACK(nowledgement)从消费者发送给RabbitMQ一个消息确认当前消息已被接收和处理,RabbitMQ可自由将其删除。
如果消费者死亡(其信道被关闭,关闭连接,或TCP连接丢失),而不发送ACK,RabbitMQ知道消息并没有被接收和执行完全,将重新将它放入队列。如果同一时间存在其他在线的消费者,它将迅速重新传递消息给另一个消费者。这样,你可以肯定没有消息丢失,即使偶尔的消费者死亡。
目前没有任何消息超时,当消费者挂掉的时候,RabbitMQ将重新传递消息,即使处理一个消息需要很长很长的时间也没关系。
消息确认默认情况下开启。在前面的例子中,我们明确地通过AUTOA​​CK = true标志将它们关闭。现在是时候删除此标志,一旦我们与任务完成,将从worker发送适当的确认。

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

使用此代码,我们可以肯定,即使你使用CTRL + C,杀死一个worker,什么都不会丢失。worker死亡后不久,所有未确认的消息会被重新传递。

被遗忘的确认
忘记baseACK是一个常见的错误,这是个简单的错误,但是后果是很严重的。当你的客户端退出的时候(可能看起来就像是随机交还)消息将被重新传递,但RabbitMQ会消耗的越来越多的内存,它将无法释放任何unacked的消息。
为了调试这种错误,你可以使用rabbitmqctl打印messages_unacknowledged字段。
$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello 0 0
...done.

消息持久化

我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ的服务器停止,我们的任务仍然会丢失。
当RabbitMQ的退出或崩溃时,除非你告诉它不要忘记的队列和消息。两件事情都需要确保,消息才不会丢失:我们需要将队列和消息持久化。
首先,我们需要确保的RabbitMQ永远不会失去我们的队列。为了做到这一点,我们需要把它声明为持久:

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

虽然以上的代码本身是对的,但是它不会对我们的目前设置起作用。这是因为我们已经定义了一个名为hello的不持久的队列,RabbitMQ不允许你使用不同的参数重新定义现有队列,并会返回一个错误。但是有一个快速的解决
办法 - 让我们与声明不同名称的队列,例如task_queue:

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

queueDeclare变化需要被施加到生产者和消费者代码两者。
在这一点上我们确保即使RabbitMQ的重启task_queue队列也不会被丢失。现在,我们需要我们的消息标记为持久性 - 通过设置MessageProperties(实现BasicProperties)的值PERSISTENT_TEXT_PLAIN。

import com.rabbitmq.client.MessageProperties;

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

注意消息持久性
将消息标记为持久性并不能完全保证信息不会丢失。虽然RabbitMQ消息将保存到磁盘,但是有很短的时间内RabbitMQ已经接受了消息,但是还没来得及保存它。此外,RabbitMQ没有为每条消息做FSYNC(2) - 它可能只是保存到缓存,并没有真正写入磁盘。持久性的保证不强,但是对于我们简单的任务队列还是绰绰有余的。如果你需要一个更强有力的保证,那么你可以使用publisher confirms

公平调度

你可能已经注意到,调度仍然没有完全按照我们真正想要的工作。举个例子,比如有两个消费者的情况,当奇数的消息非常重,但是偶数的消息非常轻的时候,一个消费者将被累死,而另一个却闲着。RabbitMQ却不知道,仍然在均匀的给每个消费者发送消息。
这种情况发生是因为RabbitMQ只负责分发进入到队列的消息,它不看为消费者未确认的消息的数量。它只是盲目分派每第n个消息给第n消费者。

![](http://images2015.cnblogs.com/blog/658141/201608/658141-20160817001222234-1482830901.png)

为了杜绝那种情况,我们可以使用basicQos方法与prefetchCount = 1设置。它告诉RabbitMQ不要把多个消息在同一时间给一个消费者。或者,换句话说,只有消费者处理并且确认前一个消息之后才会给它分配下一个消息,相反,消息将被非配给下一个不处于忙碌的消费者。

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

注意队列大小
如果所有的worker都在忙,你的队列也填满了。您将要留意的是,也许添加更多的worker,或者有一些其他的策略。

代码整合

NewTask.java

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

Worker.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);
        }
      }
    };
    channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
  }

  private static void doWork(String task) {
    for (char ch : task.toCharArray()) {
      if (ch == '.') {
        try {
          Thread.sleep(1000);
        } catch (InterruptedException _ignored) {
          Thread.currentThread().interrupt();
        }
      }
    }
  }
}

原文地址:RabbitMQ之Work Queues

代码地址:https://github.com/aheizi/hi-mq

相关:
1.RabbitMQ之HelloWorld
2.RabbitMQ之任务队列
3.RabbitMQ之发布订阅
4.RabbitMQ之路由(Routing)
5.RabbitMQ之主题(Topic)
6.RabbitMQ之远程过程调用(RPC)

posted @ 2016-08-17 00:14  aheizi  阅读(1894)  评论(0编辑  收藏