介绍
Work queues,也被称为(Task queues)任务模型。当消息处理比较耗时的时候,可以生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work模型:**让多个消费者绑定到一个队列,共同消费队列中的消息。**队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。 如下图所示:
角色:
- P: 生产者:任务的发布者。
- C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
- C2:消费者-2,领取任务并完成任务,假设完成速度快。
工作模型之平均消费消息
1.先编写一个RabbitMQ工具类,封装一些共用的方法。
public class RabbitMQUtils {
private static ConnectionFactory connectionFactory;
static {
//创建连接mq的连接工厂对象
connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("172.16.114.135");
//设置端口号
connectionFactory.setPort(5672);
//设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//设置访问虚拟主机的用户名和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
}
public static Connection getConnection(){
try {
return connectionFactory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void closeChannelAndConnection(Channel channel,Connection connection){
try {
if(channel!=null){
channel.close();
}
if(connection!=null){
connection.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
2.生产者(Provider.java)
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
//获取连接中的通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
channel.queueDeclare("work",true,false,true,null);
//发布消息---循环发布
for (int i = 0; i < 10; i++) {
channel.basicPublish("","work", null,(i+"hello work queue").getBytes());
}
RabbitMQUtils.closeChannelAndConnection(channel,connection);
}
}
3.消费者1(Customer1.java)
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
//创建通道
Channel channel = connection.createChannel();
//通道绑定队列 //这里定义队列的方式需要和生产者定义队列的方式一致,不然可能会出现问题
channel.queueDeclare("work",true,false,true,null);
//消费消息
//参数1:消费哪个队列的消息 说白了就是队列的名称
//参数2:开启消息的自动确认机制
//参数3:消费消息时的回调接口
channel.basicConsume("work",true,new DefaultConsumer(channel){
//处理消息的回调
@Override //最后一个参数: 消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1 "+ new String(body));
}
});
}
}
4.消费者2(Customer2.java)
public class Customer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
//创建通道
Channel channel = connection.createChannel();
//通道绑定队列 //这里定义队列的方式需要和生产者定义队列的方式一致,不然可能会出现问题
channel.queueDeclare("work",true,false,true,null);
//消费消息
//参数1:消费哪个队列的消息 说白了就是队列的名称
//参数2:开启消息的自动确认机制
//参数3:消费消息时的回调接口
channel.basicConsume("work",true,new DefaultConsumer(channel){
//处理消息的回调
@Override //最后一个参数: 消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2 "+new String(body));
}
});
}
}
我们先运行两个消费者,在运行生产者。两个消防者都会得到消息,分配情况如下:
可以看到,默认情况下,RabbitMQ将按顺序将每个消息发送给下一个消费者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。
那现在有这么一种情况,如果消费者1执行过慢,那么消费者2执行完之后就没事情可做了。因为任务是平均分配的,会导致资源浪费问题。接下来我们来模仿一下这种情况,给消费者1消费数据的时候加上延迟执行,修改后代码如下:
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
//创建通道
Channel channel = connection.createChannel();
//通道绑定队列 //这里定义队列的方式需要和生产者定义队列的方式一致,不然可能会出现问题
channel.queueDeclare("work",true,false,true,null);
//消费消息
//参数1:消费哪个队列的消息 说白了就是队列的名称
//参数2:开启消息的自动确认机制
//参数3:消费消息时的回调接口
channel.basicConsume("work",true,new DefaultConsumer(channel){
//处理消息的回调
@Override //最后一个参数: 消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者-1 "+ new String(body));
}
});
}
}
执行代码可以发现,消费者1还在缓慢执行,消费者2已经执行完毕了。 那么这有什么办法可以解决这个问题? 那就需要用到工作队列中的能者多劳体现。也就是说,消费者2执行完之后去帮助消费者1去执行消息。
工作模型消息确认机制和能者多劳实现
在实现之前,我们需要先来了解一下工作队列中的消息确认机制。
channel.basicConsume("work",true,new DefaultConsumer(channel){
消费消息方法中的参数2 true:就是开启自动消息确认机制,false:关闭自动消息确认机制
比如说,有这么一种情况,消费者1在消费消息的时候,在执行到某一条消息的时候发生了中断,导致后面的消息没有执行,由于我们上面的代码开启了消息自动确认机制。 一旦RabbitMQ交付了一个消息给消费者,会马上从队列中移除这个消息。在这种情况下,消费者1在执行消息的过程发生了中断,我们会丢失它正在处理的消息。也会丢失已经转发给消费者1且它还未执行的消息。
为了解决消息丢失的情况(当消费者1被杀死时,可以将任务传递给消费者2)。RabbitMQ支持消息应答,也就是说我们可以在消费者消费完一条消息后,手动确认这条消息已经被消费了。如果消费者被杀死而没有发送应答,RabbitMQ会认为该信息没有被完全的处理,然后将会重新转发给别的消费者。通过这种方式,你可以确认信息不会被丢失,即使消者偶尔被杀死。
代码实现如下:
Customer1.java
public class Customer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
//创建通道
final Channel channel = connection.createChannel();
//通道绑定队列 //这里定义队列的方式需要和生产者定义队列的方式一致,不然可能会出现问题
channel.queueDeclare("work",true,false,true,null);
//消费消息
//参数1:消费哪个队列的消息 说白了就是队列的名称
//参数2:开启消息的自动确认机制
//参数3:消费消息时的回调接口
channel.basicQos(1);
channel.basicConsume("work",false,new DefaultConsumer(channel){
//处理消息的回调
@Override //最后一个参数: 消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者-1 "+ new String(body));
//参数1: 确认队列中哪个具体消息被消费了 参数2:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
Customer2.java
public class Customer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
//创建通道
final Channel channel = connection.createChannel();
//通道绑定队列 //这里定义队列的方式需要和生产者定义队列的方式一致,不然可能会出现问题
channel.queueDeclare("work",true,false,true,null);
//消费消息
//参数1:消费哪个队列的消息 说白了就是队列的名称
//参数2:开启消息的自动确认机制 false 关闭消息自动确认机制
//参数3:消费消息时的回调接口
//每次消费一条消息
channel.basicQos(1);
channel.basicConsume("work",false,new DefaultConsumer(channel){
//处理消息的回调
@Override //最后一个参数: 消息队列中取出的消息
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2 "+new String(body));
//手动确认消费已经消费完毕
//参数1: 确认队列中哪个具体消息被消费了 参数2:是否开启多个消息同时确认
//envelope.getDeliveryTag()可以获取到当前消费的消息
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
重新执行消费1和消费2,可以发现消费者2完全的体现了能者多劳
channel.basicQos(1)的作用:
- 告诉RabbitMQ不要在同一时间给一个消费者超过一条消息。也就是说,只有消费者在空闲的时候会发送下一条消息。