在入门程序中,我们是使用的一个生产者,一个消费者。试想:如果有几个消息都需要处理,且每个消息的处理时间很长,仅有一个消费者,那么当它在处理一个消息的时候,其他消息就只有等待。
等待有时候是好的,但在程序中并不那么好,当队列中有多个消息待处理,将其分发给多个消费者,当一个消费者在处理的时候,有其他消费者继续消费队列中的消息,便缓解了等待的尴尬。
那么这篇文章将实现一个生产者,多个消费者的模式,实现任务分发:work模式,如图所示。
这样看来,我们写的入门程序其实就是Work模式的一个特例:只有一个Worker。
要更好的了解work模式,我们需要知道RabbitMQ的一些机制,以下就从问题出发一个个的给出了解释。
问题:怎样保证消息不因消费者gg而丢失 |
处理一个消息可能会花一定的时间,万一还没处理完消费者就gg了…生产者一发送消息,便会将其标记为已删除,故最终的结果是:这条消息没有得到正确的处理。而且,指派给该消费者且尚未处理的所有消息都会gg。
解决策略:取消自动回复机制 |
为了解决消息的丢失问题,RabbitMQ提供了消息确认机制:message acknowledgments,一个消费者处理完成后,将会回传一个ack给生产者,以表示处理成功,这样生产者才可以将消息删除。
这样即使一个消费者gg了,没有回传ack,那么发送者便会重发消息到队列,如果这时候有其他的消费者服务该队列,那么便会从队列中取出消息并处理。这就保证了消息的不丢失。
自动回复机制:不管是否处理成功,还是失败,都会回复ack。
channel.basicConsume(QUEUE_NAME, true, consumer);
自动恢复机制默认是打开的,在接收端的代码最后:第二个参数为true,表示会自动回复,只要生产发送消息,就会标记删除。所以我们需要将自动回复设置为false。
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
这样来保证消息不会因为消费者的gg而丢失了。
那么取消自动回复以后,我们需要手动回复一次:
channel.basicAck(envelope.getDeliveryTag(), false);
注意当前的消息确认机制只适用于同一个channel。
问题:怎样保证消息不因生产者gg而丢失 |
我们知道了如何在消费者的角度保证消息不丢失,但如果生产者gg了呢,消息同样会丢失,生产者gg后会默认丢弃所有的消息,除非告诉它某些消息是不能丢失的。
解决策略:消息持久化 |
使用消息持久化,将消息保存到磁盘上,而不是内存中,即使生产者gg了,后面还可以通过读取磁盘来进行恢复。
要实现消息持久化,我们需要做两件事:从queue与message分别来标记持久化。
①首先:从queue角度标记为持久化
boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
声明队列时的第二个参数,设置为true。当然以上代码是有问题的,因为我们已经声明一个hello了,而且那个hello的持久化是false的,这里我们需要声明一个新的队列:queue_task
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
②从message的角度标记持久化
我们已经标记了queue为持久化,重启后会读取磁盘保存的消息,那么还需要将消息标记为持久化:通过设置MessageProperties的值为:PERSISTENT_TEXT_PLAIN
channel.basicPublish("", "task_queue",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
好了现在我们已经实现消息持久化了。
注意:消息持久化并不能完全保证消息不丢失,级生产者需要将多个message保存到磁盘上,就在保存这个时间窗口上发生了意外,消息同样会丢失,尽管这个时间很短,但还是存在。不过话说回来,尽管这个持久化机制不能百分百地保证消息不丢失,但是做一些简单的任务还是够用的。 |
问题:怎样实现消息的均匀分配 |
你可能意识到了我们的任务分发模式还不是我们想要的,举个例子,一些消息处理时间长,一些消息处理时间短,当我们把一个任务重的消息发送给了一个worker1,把一个任务轻的消息发送给了worker2,现在又来了两个消息,我们又把任务重的分给了worker1,轻的分给了worker2,这样worker1的任务就相当的重,而worker就会很闲。当然这不是我们想要的。
解决策略:单个处理原则 |
采用单个处理原则,我们每次都只分发一次任务给消费者,换句话说,如果一个消费者尚未处理完成,RabbitMQ不会分配新的消息给消费者。如图所示
代码实现:
int prefetchCount = 1;
channel.basicQos(prefetchCount );// 负载均衡
注意:如果所有的worker都在处理,queue可能出现full的情况,这是需要监控的,或者通过其他策略来调整的。 |
我们使用Thread.sleep()模拟消息处理需要时间。
生产者,我们用多任务NewTask命名:
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.MessageProperties;
public class NewTask {
private static final String QUEUE_NAME = "task_queue";
public static void main(String[] args) throws IOException, TimeoutException {
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);
for (int i = 1; i <= 10; i++) {
String message = "the message" + i;
//标记message持久化
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
}
channel.close();
connection.close();
}
}
消费者1-Work1
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.AMQP.BasicProperties;
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 Work1 {
private static final String QUEUE_NAME = "task_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
//标记queue持久化
boolean durable = true;
channel.queueDeclareNoWait(QUEUE_NAME, durable, false, false, null);
channel.basicQos(1);// 负载均衡
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//手动回复一次
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;// 自动回复确认,默认为true,生产者已发送就会将其标记为:已删除
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
protected static void doWork(String message) throws InterruptedException {
Thread.sleep(1000);
System.out.println("work1收到消息:" + message);
}
}
消费者2-Work2,与Work1代码一模一样,除了涉及到的名字不同。
运行与结果:
①首先运行NewTask,再运行Work1(不运行Work2)
②登录管理端,删除task_queue
首先运行NewTask,再运行Work1,再运行Work2
可以看到Work2处理了消息:4、6、8、10,没有处理2是因为我们只运行Work1与Work2的时间间隔中,Work1多运行了一些。而且由于console的覆盖,我们也看不到Work1的打印了,如果要完全在控制台看到平均的处理结果,可以使用多个ide运行。
下一篇我们将一起来探索:发布/订阅 模式