一、概述和模型
- 工作队列,多个消费者处理同一个消息队列,每条消息只会发给其中一个消费者。工作队列有两种处理方式,Round-robin(轮询分发)和Fair dispatch(公平分发)。应用场景,订单系统中多台服务器处理上游系统的下发过来的订单(个人认为原理跟负载均衡类似)。
二、轮询分发 Round-robin
- 轮询分发,即将信息平分成N份,并推送给N个消费者。
- 轮询分发实现跟简单队列类似,只是多了N个消费者
- 创建MQ连接工具类
public class RabbitmqUntil {
//获取连接
public static Connection getRabbitmqConnection() throws Exception{
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("127.0.0.1");
//设置端口(这里的端口号指定是AMQP协议所用的端口号)
factory.setPort(5672);
//设置数据库
factory.setVirtualHost("/test");
//设置用户名
factory.setUsername("test");
//设置密码
factory.setPassword("test");
return factory.newConnection();
}
}
- 创建消息发送者
public class WorkSender {
public static final String QUEUE_NAME="work query";
//发送信息
public void sendMessage() throws Exception{
//1、获取连接
Connection rabbitmqConnection = RabbitmqUntil.getRabbitmqConnection();
//2、获取管道
Channel channel = rabbitmqConnection.createChannel();
/**
* 3、队列创建(如果队列存在,则不进行创建)
* 参数1:队列名
* 参数2:是否支持持久化,默认队列在内存中,重启会丢失。设置为true,会保存到erlang自带的数据库中
* 参数3:当连接关闭后是否自动删除队列;是否私有队列,如果私有,其他通道不能访问当前队列
* 参数4:当没有消费者,是否自动删除队列
* 参数5:其他参数(通过BasicProperties进行传输)
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、发布消息
for (int i = 0; i < 50; i++) {
String message="hello ,I am work queue"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
//5、关闭连接
channel.close();
rabbitmqConnection.close();
}
public static void main(String[] args)throws Exception {
WorkSender workSender = new WorkSender();
workSender.sendMessage();
}
}
- 创建两个消费者
public class WorkReceiver1 {
//队列名称
public static final String QUEUE_NAME="work query";
//接收信息
public void receiveMessage()throws Exception{
//1、获取连接
Connection rabbitmqConnection = RabbitmqUntil.getRabbitmqConnection();
//2、获取管道
Channel channel = rabbitmqConnection.createChannel();
//3、创建队列(如果队列已存在,则不进行处理)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、创建消费用户
DefaultConsumer consumer = 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,"utf-8"));
//模拟业务处理,0.5秒
try {
Thread.sleep(500);
}catch (Exception e){
e.printStackTrace();
}
}
};
/**
* 监听rabbitmq队列信息(此时程序会自动堵塞,当有数据到达时,处理相应的数据)
* 参数1:队列名
* 参数2:是否自动问答
* 参数3:消费用户
*/
channel.basicConsume(QUEUE_NAME,true,consumer);
}
public static void main(String[] args)throws Exception {
WorkReceiver1 workReceiver1 = new WorkReceiver1();
workReceiver1.receiveMessage();
}
}
public class WorkReceiver2 {
//队列名称
public static final String QUEUE_NAME="work query";
//接收信息
public void receiveMessage()throws Exception{
//1、获取连接
Connection rabbitmqConnection = RabbitmqUntil.getRabbitmqConnection();
//2、获取管道
Channel channel = rabbitmqConnection.createChannel();
//3、创建队列(如果队列已存在,则不进行处理)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、创建消费用户
DefaultConsumer consumer = 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,"utf-8"));
//模拟业务处理,1秒
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
};
/**
* 监听rabbitmq队列信息(此时程序会自动堵塞,当有数据到达时,处理相应的数据)
* 参数1:队列名
* 参数2:是否自动问答
* 参数3:消费用户
*/
channel.basicConsume(QUEUE_NAME,true,consumer);
}
public static void main(String[] args)throws Exception {
WorkReceiver2 workReceiver2 = new WorkReceiver2();
workReceiver2.receiveMessage();
}
}
- 运行main函数,可以看到,即使两个消费者处理消息的时间不一样,但是每个消费者都得到相同的消息。这就是轮询分发
三、公平分发 Fair dispatch
- 公平分发,在多个消费者中,谁将消息处理完了,就将消息推送给该消费者,这里涉及到消息应答,即消费处理完成后,发送一个响应告诉rabbitmq。
- 创建消费者(每次只给消费者发送一条消息)
public class WorkFairSender {
//队列名称
public static final String QUEUE_NAME="work fair query";
//发送信息
public void sendMessage() throws Exception{
//1、获取连接
Connection rabbitmqConnection = RabbitmqUntil.getRabbitmqConnection();
//2、获取管道
Channel channel = rabbitmqConnection.createChannel();
//3、创建队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、设置每次发送信息不超过1条
channel.basicQos(1);
//5、发布消息
for (int i = 0; i < 50; i++) {
String message="hello ,I am work queue"+i;
System.out.println("发布信息:"+message);
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
public static void main(String[] args)throws Exception {
WorkFairSender workFairSender = new WorkFairSender();
workFairSender.sendMessage();
}
}
- 创建两个消费者(使用消息应答机制)
public class WorkFairReceiver1 {
//队列名称
public static final String QUEUE_NAME="work fair query";
//接收消息
public void receiveMessage()throws Exception{
//1、获取连接
Connection rabbitmqConnection = RabbitmqUntil.getRabbitmqConnection();
//2、获取管道
final Channel channel = rabbitmqConnection.createChannel();
//3、创建队列(如果队列已存在,则不进行处理)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、设置每次处理息不超过1条
channel.basicQos(1);
//5、创建消费用户
DefaultConsumer consumer = 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,"utf-8"));
try {
Thread.sleep(500);
}catch (Exception e){
e.printStackTrace();
}finally {
//处理完成后给rabbitmq一个应答响应
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
/**
* 6、监听rabbitmq队列信息(此时程序会自动堵塞,当有数据到达时,处理相应的数据)
* 参数1:队列名
* 参数2:是否自动问答(设置为非自动问答)
* 参数3:消费用户
*/
channel.basicConsume(QUEUE_NAME,false,consumer);
}
public static void main(String[] args)throws Exception {
WorkFairReceiver1 workFairReceiver1 = new WorkFairReceiver1();
workFairReceiver1.receiveMessage();
}
}
public class WorkFairReceiver2 {
//队列名称
public static final String QUEUE_NAME="work fair query";
//接收消息
public void receiveMessage()throws Exception{
//1、获取连接
Connection rabbitmqConnection = RabbitmqUntil.getRabbitmqConnection();
//2、获取管道
final Channel channel = rabbitmqConnection.createChannel();
//3、创建队列(如果队列已存在,则不进行处理)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、设置每次处理息不超过1条
channel.basicQos(1);
//5、创建消费用户
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("公平分发接收者2:"+new Date().getTime()+" "+new String(body,"utf-8"));
try {
Thread.sleep(10000);
}catch (Exception e){
e.printStackTrace();
}finally {
//处理完成后给rabbitmq一个应答响应
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
/**
* 6、监听rabbitmq队列信息(此时程序会自动堵塞,当有数据到达时,处理相应的数据)
* 参数1:队列名
* 参数2:是否自动问答(设置为非自动问答)
* 参数3:消费用户
*/
channel.basicConsume(QUEUE_NAME,false,consumer);
}
public static void main(String[] args)throws Exception {
WorkFairReceiver2 workFairReceiver2 = new WorkFairReceiver2();
workFairReceiver2.receiveMessage();
}
}
代码解析:
- 在发送者中添加channel.basicQos(1),表示每次只推送给消费者一条消息
- 在消费者中添加channel.basicQos(1),表示每次只处理一条消息
- 消费这处理完成后给rabbitmq一个应答响应channel.basicAck(envelope.getDeliveryTag(),false)
- 监听rabbitmq队列是,是否自动问答设置为false channel.basicConsume(QUEUE_NAME,false,consumer)
- 运行程序,就会发现消费者1处理的消息比消费者多,这就是公平分发。
注:
- 当程序设置为自动问答的时候,队列中消息被获取的时候,消息会自动被删除,如果这时消费者处理出现异常导致系统奔溃,此时数据丢失。非自动问答,需要在得到消费者的应答相应后才会删除队列中的消息,所以非自动问答,在一定程度上可以保证数据安全
- 非自动应答在rabbitmq奔溃时,也会丢失数据,这个可用通过在定义队列的时候申明为支持持久化即可channel.queueDeclare(QUEUE_NAME,true,false,false,null)
- 如果直接在上面的例子中修改,让程序支持持久化channel.queueDeclare(QUEUE_NAME,true,false,false,null),程序会报错,因为已经存在的队列不能修改其属性,必须先删掉原本的队列再进行修改