上一篇文章我们讲了helloword模式的消息发布:RabbitMq 七大发布消息之helloword最简单的发布和消费消息
这一篇我们讲解第二种消息发布模式——Work queues,也就是项目中常用的Task queues模式。从以下图中不难看出是属于一个生产者对应多个消费者模式,是一对多的关系。
Work queues,也被成为Task queues,任务模型。当消息处理比较耗时的时候,可能生产消息的速度远远大于消费者消费的速度,那么就会造成队列中的消息发生堆积,无法及时被处理。此时就可以使用Work queues,让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦被消费就会消失,因此不会造成重复消费的情况。
我们在上一篇helloword的基础上进行优化,先提出一个公共类的代码。
1.RabbitMqUtil
package utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Author:
* @Despriction:
* @Package: utils
* @Date:Created in 2020/3/23 8:44
* @Modify By:
*/
public class RabbitMqUtil {
private static ConnectionFactory connectionFactory;
static {
//第一步:创建rabbitmq的连接工厂
connectionFactory = new ConnectionFactory();
//第二步:设置连接的rabbitmq服务器的连接ip
connectionFactory.setHost("192.168.4.6");
//第三步:连接端口号,默认的为5672
connectionFactory.setPort(5672);
//第四步:设置连接哪个虚拟主机
connectionFactory.setVirtualHost("/ems");
//第五步:设置连接虚拟主机的用户和密码
connectionFactory.setUsername("ems");
connectionFactory.setPassword("123");
}
//获取rabbitmq的连接
public static Connection getConnection() {
//第六步:获取连接对象
Connection connection = null;
try {
connection = connectionFactory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
public static void getClose(Connection connection, Channel channel) {
try {
if (channel != null) {
channel.close();
}
if (connection != null) {
connection.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
在项目中rabbitmq的连接将会以配置文件的形式进行配置,此处只是简单的学习使用,用静态代码块的原因是为了只需要开辟一次空间即可,且只会在类加载的时候执行一次,此后不再执行。
2.生产者代码
package workQueues;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import org.junit.Test;
import utils.RabbitMqUtil;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Author:
* @Despriction: Work queues 生产消费模式
* @Package: helloword
* @Date:Created in 2020/3/19 11:14
* @Modify By:
*/
public class Provider {
@Test
public void sendMsg() throws IOException, TimeoutException {
//通过公用方法获取rabbit连接
Connection connection = RabbitMqUtil.getConnection();
//第七步:创建连接通道
Channel channel = connection.createChannel();
//第八步:通道绑定对应的消息队列
/**
* 参数1 queue:队列名称,如果队列不存在则自动创建
* 参数2 durable:用来定义队列是否需要持久化,true 需要持久化;false 不需要持久化
* 参数3 exclusive:定义是否独占队列,true 是;false 否
* 参数4 autodelete:消费完成后是否需要自动删除队列,true 是;false 否
* 参数5:自定义的额外附加参数
*/
channel.queueDeclare("work", true, false, false, null);
//第九步:发布消息
/**
* 参数1:交换机名称
* 参数2:队列名称
* 参数3:传递消息的额外设置
* MessageProperties.PERSISTENT_TEXT_PLAIN 表示该队列中的未被消费的消息会进行持久化
* 参数4:消息的具体内容,必须二进制
*/
//for循环测试一个生产者对应多个消费者时,消息队列中的消息是怎样分配的
for (int i = 0; i < 10; i++) {
channel.basicPublish("", "work", MessageProperties.PERSISTENT_TEXT_PLAIN, ("第(" + i + ")条Work queues").getBytes());
}
//第十步:关闭通道和连接
//channel.close();
//connection.close();
RabbitMqUtil.getClose(connection, channel);
}
}
此处使用for循环生产多个消息为了方便测试。
3.消费者1和消费2的代码
package workQueues;
import com.rabbitmq.client.*;
import utils.RabbitMqUtil;
import java.io.IOException;
/**
* @Author:
* @Despriction:
* @Package: workQueues
* @Date:Created in 2020/3/23 9:05
* @Modify By:
*/
public class Consumer1 {
public static void main(String args[]) throws IOException {
//通过公用方法获取rabbit连接
Connection connection = RabbitMqUtil.getConnection();
//创建通道连接
Channel channel = connection.createChannel();
//通道绑定队列
/**
* 参数1 queue:队列名称,如果队列不存在则自动创建
* 参数2 durable:用来定义队列是否需要持久化,true 需要持久化;false 不需要持久化
* 参数3 exclusive:定义是否独占队列,true 是;false 否
* 参数4 autodelete:消费完成后是否需要自动删除队列,true 是;false 否
* 参数5:自定义的额外附加参数
*/
channel.queueDeclare("work",true,false,false,null);
//消费消息
/**
* 参数1:队列名称,即将要消费哪一个队列的消息
* 参数2:开启消息的自动确认机制
* 参数3:消费时的回调接口,常用来处理业务逻辑,一般都是Consumer接口的实现类,此处用默认的DefaultConsumer
*/
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));
}
});
}
}
package workQueues;
import com.rabbitmq.client.*;
import utils.RabbitMqUtil;
import java.io.IOException;
/**
* @Author:
* @Despriction:
* @Package: workQueues
* @Date:Created in 2020/3/23 9:05
* @Modify By:
*/
public class Consumer2 {
public static void main(String args[]) throws IOException {
//通过公用方法获取rabbit连接
Connection connection = RabbitMqUtil.getConnection();
//创建通道连接
Channel channel = connection.createChannel();
//通道绑定队列
/**
* 参数1 queue:队列名称,如果队列不存在则自动创建
* 参数2 durable:用来定义队列是否需要持久化,true 需要持久化;false 不需要持久化
* 参数3 exclusive:定义是否独占队列,true 是;false 否
* 参数4 autodelete:消费完成后是否需要自动删除队列,true 是;false 否
* 参数5:自定义的额外附加参数
*/
channel.queueDeclare("work",true,false,false,null);
//消费消息
/**
* 参数1:队列名称,即将要消费哪一个队列的消息
* 参数2:开启消息的自动确认机制
* 参数3:消费时的回调接口,常用来处理业务逻辑,一般都是Consumer接口的实现类,此处用默认的DefaultConsumer
*/
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));
}
});
}
}
4.执行之后控制台的打印
消费者1控制台的打印
消费者2控制台的打印
可以看到这两个消费者的控制台打印的东西都是平均打印的,即队列的消息分配在默认情况下是均衡分配的。且在官方文档上是明确说明的:https://www.rabbitmq.com/tutorials/tutorial-two-java.html
以上这种情况也会造成不友好的情况,一旦某一个消费者出现网络延迟等问题就会造成一部分的消息丢失,尽管被消费者消费了,但是却没法进行业务逻辑的处理,客户没法获取到消息的反馈,因此这部分消息就被丢失了。
例如我们此时将消费者1的代码里面加入线程阻塞来进行实验。代码如下:
package workQueues;
import com.rabbitmq.client.*;
import utils.RabbitMqUtil;
import java.io.IOException;
/**
* @Author:
* @Despriction:
* @Package: workQueues
* @Date:Created in 2020/3/23 9:05
* @Modify By:
*/
public class Consumer1 {
public static void main(String args[]) throws IOException {
//通过公用方法获取rabbit连接
Connection connection = RabbitMqUtil.getConnection();
//创建通道连接
Channel channel = connection.createChannel();
//通道绑定队列
/**
* 参数1 queue:队列名称,如果队列不存在则自动创建
* 参数2 durable:用来定义队列是否需要持久化,true 需要持久化;false 不需要持久化
* 参数3 exclusive:定义是否独占队列,true 是;false 否
* 参数4 autodelete:消费完成后是否需要自动删除队列,true 是;false 否
* 参数5:自定义的额外附加参数
*/
channel.queueDeclare("work",true,false,false,null);
//消费消息
/**
* 参数1:队列名称,即将要消费哪一个队列的消息
* 参数2:开启消息的自动确认机制
* 参数3:消费时的回调接口,常用来处理业务逻辑,一般都是Consumer接口的实现类,此处用默认的DefaultConsumer
*/
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消费的速度缓慢,也会造成消费者1的信息堆积,一旦发生网络异常,还未被消费的或者还未被业务逻辑处理的消息就会被丢失。以下是官方文档的叙述:
如何改进?
一是让每个消费者每次只消费一条消息,而不是像之前一次性打包消费;
//表示该消费者每次只能消费处理一个消息
channel.basicQos(1);
二是将自动消费确认true改为false,即由自动确认更改为手动确认,手动确认完成之后才算该消息在整个流程中完成了可以消费下一个消息了。
//消费消息
/**
* 参数1:队列名称,即将要消费哪一个队列的消息
* 参数2:开启消息的自动确认机制
* 参数3:消费时的回调接口,常用来处理业务逻辑,一般都是Consumer接口的实现类,此处用默认的DefaultConsumer
*/
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:是否开启多个消息同时确认机制,false 每次确认1个
*/
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
一定不要漏了:
//手动确认
/**
* 参数1:手动确认的标识
* 参数2:是否开启多个消息同时确认机制,false 每次确认1个
*/
channel.basicAck(envelope.getDeliveryTag(),false);
最后给出优化之后的消费者1和消费者2的详细代码,我了有所比较,依然将消费者1进行线程阻塞。
消费者1:
package workQueues;
import com.rabbitmq.client.*;
import utils.RabbitMqUtil;
import java.io.IOException;
/**
* @Author: [email protected]
* @Despriction:
* @Package: workQueues
* @Date:Created in 2020/3/23 9:05
* @Modify By:
*/
public class Consumer1 {
public static void main(String args[]) throws IOException {
//通过公用方法获取rabbit连接
Connection connection = RabbitMqUtil.getConnection();
//创建通道连接
final Channel channel = connection.createChannel();
//表示该消费者每次只能消费处理一个消息
channel.basicQos(1);
//通道绑定队列
/**
* 参数1 queue:队列名称,如果队列不存在则自动创建
* 参数2 durable:用来定义队列是否需要持久化,true 需要持久化;false 不需要持久化
* 参数3 exclusive:定义是否独占队列,true 是;false 否
* 参数4 autodelete:消费完成后是否需要自动删除队列,true 是;false 否
* 参数5:自定义的额外附加参数
*/
channel.queueDeclare("work",true,false,false,null);
//消费消息
/**
* 参数1:队列名称,即将要消费哪一个队列的消息
* 参数2:开启消息的自动确认机制
* 参数3:消费时的回调接口,常用来处理业务逻辑,一般都是Consumer接口的实现类,此处用默认的DefaultConsumer
*/
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:是否开启多个消息同时确认机制,false 每次确认1个
*/
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
消费者2:
package workQueues;
import com.rabbitmq.client.*;
import utils.RabbitMqUtil;
import java.io.IOException;
/**
* @Author: [email protected]
* @Despriction:
* @Package: workQueues
* @Date:Created in 2020/3/23 9:05
* @Modify By:
*/
public class Consumer2 {
public static void main(String args[]) throws IOException {
//通过公用方法获取rabbit连接
Connection connection = RabbitMqUtil.getConnection();
//创建通道连接
final Channel channel = connection.createChannel();
//表示该消费者每次只能消费处理一个消息
channel.basicQos(1);
//通道绑定队列
/**
* 参数1 queue:队列名称,如果队列不存在则自动创建
* 参数2 durable:用来定义队列是否需要持久化,true 需要持久化;false 不需要持久化
* 参数3 exclusive:定义是否独占队列,true 是;false 否
* 参数4 autodelete:消费完成后是否需要自动删除队列,true 是;false 否
* 参数5:自定义的额外附加参数
*/
channel.queueDeclare("work",true,false,false,null);
//消费消息
/**
* 参数1:队列名称,即将要消费哪一个队列的消息
* 参数2:开启消息的自动确认机制
* 参数3:消费时的回调接口,常用来处理业务逻辑,一般都是Consumer接口的实现类,此处用默认的DefaultConsumer
*/
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:是否开启多个消息同时确认机制,false 每次确认1个
*/
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
控制台打印-----
消费者1控制台打印:
消费者2控制台打印:
可以看到由于消费者1我们进行了线程阻塞来模拟网络延迟等现象,而消费者二正常执行,最终结果,消费者1只消费了一个,而消费者2消费了9条消息,最终实现了能者多劳的效果,满足业务场景。