java消息分发机制_RabbitMQ消息队列:任务分发机制

本文介绍了如何使用RabbitMQ实现任务分发,包括循环分发(round-robin)、消息确认(message acknowledgment)和公平分发(fair dispatch)。通过Java示例展示了如何设置消息持久化和预取值,以确保高并发和数据安全。
摘要由CSDN通过智能技术生成

我们解决了从发送端(Producer)向接收端(Consumer)发送“Hello World”的问题。在实际的应用场景中,这是远远不够的。从本篇文章开始,我们将结合更加实际的应用场景来讲解更多的高级用法。

有时Consumer需要大量的运算时,RabbitMQ Server需要一定的分发机制来balance每个Consumer的load。试想一下,对于web application来说,在一个很多的HTTP request里是没有时间来处理复杂的运算的,只能通过后台的多个工作线程来完成,队列中的任务将会被工作线程共享执行,这样的概念在web应用这非常有用。接下来我们分布讲解。

应用场景就是RabbitMQ Server会将queue的Message分发给不同的Consumer以处理计算密集型的任务:

87462a3ba3423cd4eac524dd97519b64.png

1. 准备

实际应用Consumer可能做的是计算密集型的工作,那就不能简单的字符串了。在现实应用中,Consumer有可能做的是一个图片的resize,或者是pdf文件的渲染或者内容提取。但是作为Demo,还是用字符串模拟吧:通过字符串中的.的数量来决定计算的复杂度,每个.都会消耗1s,即sleep(1)。

发送端:

package com.zhy.rabbitMq._02_workqueue;

import java.io.IOException;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

public class NewTask{

//队列名称

private final static String QUEUE_NAME = "queue2";

public static void main(String[] args) throws IOException{

//创建连接和频道

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("localhost");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

//声明队列

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

//发送10条消息,依次在消息后面附加1-10个点

for (int i = 0; i < 10; i++){

String dots = "";

for (int j = 0; j <= i; j++){

dots += ".";

}

String message = "helloworld" + dots+dots.length();

channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

System.out.println(" [x] Sent '" + message + "'");

}

//关闭频道和资源

channel.close();

connection.close();

}

}

接收端:

package com.zhy.rabbitMq._02_workqueue;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import com.rabbitmq.client.QueueingConsumer;

public class Work{

//队列名称

private final static String QUEUE_NAME = "workqueue";

public static void main(String[] argv) throws java.io.IOException,

java.lang.InterruptedException{

//区分不同工作进程的输出

int hashCode = Work.class.hashCode();

//创建连接和频道

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("localhost");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

//声明队列

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

System.out.println(hashCode

+ " [*] Waiting for messages. To exit press CTRL+C");

QueueingConsumer consumer = new QueueingConsumer(channel);

// 指定消费队列

channel.basicConsume(QUEUE_NAME, true, consumer);

while (true){

QueueingConsumer.Delivery delivery = consumer.nextDelivery();

String message = new String(delivery.getBody());

System.out.println(hashCode + " [x] Received '" + message + "'");

doWork(message);

System.out.println(hashCode + " [x] Done");

}

}

/**

* 每个点耗时1s

* @param task

* @throws InterruptedException

*/

private static void doWork(String task) throws InterruptedException{

for (char ch : task.toCharArray()){

if (ch == '.')

Thread.sleep(1000);

}

}

}

2. Round-robin dispatching 循环分发

RabbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的。如果现在load加重,那么只需要创建更多的Consumer来进行任务处理即可。当然了,对于负载还要加大怎么办?我没有遇到过这种情况,那就可以创建多个virtual Host,细化不同的通信类别了。

首先开启个Consumer,即运行两个工作者。

[x] Sent 'helloworld.1'

[x] Sent 'helloworld..2'

[x] Sent 'helloworld...3'

[x] Sent 'helloworld....4'

工作者1:

605645 [*] Waiting for messages. To exit press CTRL+C

605645 [x] Received 'helloworld.1'

605645 [x] Done

605645 [x] Received 'helloworld....3'

605645 [x] Done

工作者2:

18019860 [*] Waiting for messages. To exit press CTRL+C

18019860 [x] Received 'helloworld..2'

18019860 [x] Done

18019860 [x] Received 'helloworld.....4'

18019860 [x] Done

可以看到,默认的,RabbitMQ会一个一个的发送信息给下一个消费者(consumer),而不管每个任务的时长等等,且是一次性分配,并非一个一个分配。平均的每个消费者将会获得相等数量的消息。这样分发消息的方式叫做round-robin。中种分发还有问题,接着了解吧!

3. Message acknowledgment 消息确认

每个Consumer可能需要一段时间才能处理完收到的数据。你可能担心一个工作者(Consumer)在这个过程中出错了,异常退出了,而数据还没有处理完成,那么非常不幸这段数据就丢失了。因为我们采用no-ack的方式进行确认,一旦RabbitMQ交付了一个消息给消费者,会马上从内存中移除这条信息。也就是说,每次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足够长的时间来做数据处理。

默认情况下,消息确认是打开的(enabled)。上面代码中我们通过autoAsk= True 关闭了ack。重新修改一下callback,以在消息处理完成后发送ack:

booleanack =false;//打开应答机制

channel.basicConsume(QUEUE_NAME, ack, consumer);

//另外需要在每次处理完成一个消息后,手动发送一次应答。

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

这样即使你通过Ctr-C中断了Consumer,那么Message也不会丢失了,它会被分发到下一个Consumer。

如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于RabbitMQ会长时间运行,因此这个“内存泄漏”是致命的。去调试这种错误,可以通过一下命令打印

$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

Listing queues ...

hello    0       0

...done.

4. Message durability消息持久化

我们学习了消费者被杀死,Message也不会丢失。但是如果RabbitMQ Server退出呢?软件都有bug,即使RabbitMQ Server是完美毫无bug的,它还是有可能退出的:被其它软件影响,或者系统重启了,系统panic了。。。

为了保证在RabbitMQ退出或者crash了数据仍没有丢失,需要将queue和Message都要持久化。

queue的持久化需要在声明时指定durable=True:

第一, 我们需要确认RabbitMQ永远不会丢失我们的队列。为了这样,我们需要声明它为持久化的。

boolean durable = true;

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

注:RabbitMQ不允许使用不同的参数重新定义一个队列,所以已经存在的队列,我们无法修改其属性。

第二, 我们需要标识我们的信息为持久化的。通过设置MessageProperties(implements BasicProperties)值为PERSISTENT_TEXT_PLAIN。

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

现在你可以执行一个发送消息的程序,然后关闭服务,再重新启动服务,运行消费者程序做下实验。

5. Fair dispatch 公平分发

你可能也注意到了,分发机制不是那么优雅。默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。当然n是取余后的。它不管Consumer是否还有unacked Message,只是按照这个默认机制进行分发。

那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却是毫无休息的机会。那么,RabbitMQ是如何处理这种问题呢?

be1ae8c13e9829682d0404be0f90a2f3.png

通过basic.qos方法设置prefetch_count=1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 设置方法如下:

intprefetchCount =1;

channel.basicQos(prefetchCount);

测试:改变发送消息的代码,将消息末尾点数改为3-2个,然后首先开启两个工作者,接着发送消息:

[x] Sent 'helloworld...3'

[x] Sent 'helloworld..2'

工作者1:

18019860 [*] Waiting for messages. To exit press CTRL+C

18019860 [x] Received 'helloworld...3'

18019860 [x] Done

工作者2:

31054905 [*] Waiting for messages. To exit press CTRL+C

31054905 [x] Received 'helloworld..2'

31054905 [x] Done

可以看出此时并没有按照之前的Round-robin机制进行转发消息,而是当消费者不忙时进行转发。且这种模式下支持动态增加消费者,因为消息并没有发送出去,动态增加了消费者马上投入工作。而默认的转发机制会造成,即使动态增加了消费者,此时的消息已经分配完毕,无法立即加入工作,即使有很多未完成的任务。

6. 最终版本

发送端:

package com.zhy.rabbitMq._02_workqueue;

import java.io.IOException;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import com.rabbitmq.client.MessageProperties;

public class NewTask

{

// 队列名称

private final static String QUEUE_NAME = "workqueue_persistence";

public static void main(String[] args) throws IOException

{

// 创建连接和频道

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("localhost");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

// 声明队列

boolean durable = true;// 1、设置队列持久化

channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

// 发送10条消息,依次在消息后面附加1-10个点

for (int i = 5; i > 0; i--)

{

String dots = "";

for (int j = 0; j <= i; j++)

{

dots += ".";

}

String message = "helloworld" + dots + dots.length();

// MessageProperties 2、设置消息持久化

channel.basicPublish("", QUEUE_NAME,

MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

System.out.println(" [x] Sent '" + message + "'");

}

// 关闭频道和资源

channel.close();

connection.close();

}

}

接收端:

package com.zhy.rabbitMq._02_workqueue;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import com.rabbitmq.client.QueueingConsumer;

public class Work

{

// 队列名称

private final static String QUEUE_NAME = "workqueue_persistence";

public static void main(String[] argv) throws java.io.IOException,

java.lang.InterruptedException

{

// 区分不同工作进程的输出

int hashCode = Work.class.hashCode();

// 创建连接和频道

ConnectionFactory factory = new ConnectionFactory();

factory.setHost("localhost");

Connection connection = factory.newConnection();

Channel channel = connection.createChannel();

// 声明队列

boolean durable = true;

channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

System.out.println(hashCode

+ " [*] Waiting for messages. To exit press CTRL+C");

//设置最大服务转发消息数量

int prefetchCount = 1;

channel.basicQos(prefetchCount);

QueueingConsumer consumer = new QueueingConsumer(channel);

// 指定消费队列

boolean ack = false; // 打开应答机制

channel.basicConsume(QUEUE_NAME, ack, consumer);

while (true)

{

QueueingConsumer.Delivery delivery = consumer.nextDelivery();

String message = new String(delivery.getBody());

System.out.println(hashCode + " [x] Received '" + message + "'");

doWork(message);

System.out.println(hashCode + " [x] Done");

//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

}

}

/**

* 每个点耗时1s

*

* @param task

* @throws InterruptedException

*/

private static void doWork(String task) throws InterruptedException

{

for (char ch : task.toCharArray())

{

if (ch == '.')

Thread.sleep(1000);

}

}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值