RabbitMQ学习第二记:工作队列的两种分发方式,轮询分发(Round-robin)和 公平分发(Fair dispatch)
1、什么是RabbitMQ工作队列
我们在应用程序使用消息系统时,一般情况下生产者往队列里插入数据时速度是比较快的,但是消费者消费数据往往涉及到一些业务逻辑处理导致速度跟不上生产者生产数据。因此如果一个生产者对应一个消费者的话,很容易导致很多消息堆积在队列里。这时,就得使用工作队列了。一个队列有多个消费者同时消费数据。
下图取自于官方网站(RabbitMQ)的工作队列的图例
P:消息的生产者
C1:消息的消费者1
C2:消息的消费者2
红色:队列
生产者将消息发送到队列,多个消费者同时从队列中获取消息。
工作队列有两种分发数据的方式:轮询分发(Round-robin)和 公平分发(Fair dispatch)。轮询分发:队列给每一个消费者发送数量一样的数据。公平分发:消费者设置每次从队列中取一条数据,并且消费完后手动应答,继续从队列取下一个数据。下面分别是两种分发方式不同的写法。
2、轮询分发(Round-robin)
生产者(Send)生产10条数据,消费者1(Receive1)接收数据并假设处理业务逻辑1s,消费者2(Receive2)接收数据并假设处理业务逻辑2s(生产者先运行,两个消费者同时运行)。
2.1、生产者(Send)代码
public class Send { //队列名称 private static final String QUEUE_NAME = "test_work_round_robin_queue"; public static void main(String[] args) { try { //获取连接 Connection connection = ConnectionUtil.getConnection(); //从连接中获取一个通道 Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); for (int i = 0; i < 10; i++) { String message = "this is work_round_robin queue message" + i; System.out.println("[send]:" + message); //发送消息 channel.basicPublish("", QUEUE_NAME, null, message.getBytes("utf-8")); Thread.sleep(20 * i); } channel.close(); connection.close(); } catch (IOException | TimeoutException | InterruptedException e) { e.printStackTrace(); } } }
运行结果:
[send]:this is work_round_robin queue message0
[send]:this is work_round_robin queue message1
[send]:this is work_round_robin queue message2
[send]:this is work_round_robin queue message3
[send]:this is work_round_robin queue message4
[send]:this is work_round_robin queue message5
[send]:this is work_round_robin queue message6
[send]:this is work_round_robin queue message7
[send]:this is work_round_robin queue message8
[send]:this is work_round_robin queue message9
2.2、消费者1(Receive1)代码
public class Receive1 { //队列名称 private static final String QUEUE_NAME = "test_work_round_robin_queue"; public static void main(String[] args) { try { //获取连接 Connection connection = ConnectionUtil.getConnection(); //从连接中获取一个通道 Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //定义消费者 DefaultConsumer 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"); System.out.println("[1] Receive message:" + message); try { //消费者休息1s处理业务 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("[1] done"); } } }; //监听队列 channel.basicConsume(QUEUE_NAME, true, consumer); } catch (IOException e) { e.printStackTrace(); } } }
运行结果:
[1] Receive message:this is work_round_robin queue message0
[1] done
[1] Receive message:this is work_round_robin queue message2
[1] done
[1] Receive message:this is work_round_robin queue message4
[1] done
[1] Receive message:this is work_round_robin queue message6
[1] done
[1] Receive message:this is work_round_robin queue message8
[1] done
2.3、消费者2(Receive2)代码
public class Receive2 { //队列名称 private static final String QUEUE_NAME = "test_work_round_robin_queue"; public static void main(String[] args) { try { //获取连接 Connection connection = ConnectionUtil.getConnection(); //从连接中获取一个通道 Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //定义消费者 DefaultConsumer 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"); System.out.println("[2] Receive message:" + message); try { //消费者休息2s处理业务 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("[2] done"); } } }; //监听队列 channel.basicConsume(QUEUE_NAME, true, consumer); } catch (IOException e) { e.printStackTrace(); } } }
运行结果:
[2] Receive message:this is work_round_robin queue message1
[2] done
[2] Receive message:this is work_round_robin queue message3
[2] done
[2] Receive message:this is work_round_robin queue message5
[2] done
[2] Receive message:this is work_round_robin queue message7
[2] done
[2] Receive message:this is work_round_robin queue message9
[2] done
总结:两个消费者得到的数据量一样的。从运行时可以看到消费者1会先执行完,消费者2会后执行完。并不会因为两个消费者处理数据速度不一样使得两个消费者取得不一样数量的数据。并且当队列数量大的时候通过观察RabbitMQ的管理后台,可以看到管理界面队列中的数据很快就没了,但是这个时候两个消费者其实并没有消费完数据。这种分发方式存在着很大的隐患。
3、公平分发(Fair dispatch)
生产者(Send)生产10条数据,消费者1(Receive1)接收数据并假设处理业务逻辑1s,消费者2(Receive2)接收数据并假设处理业务逻辑2s(生产者先运行,两个消费者同时运行)。
消费者设置每次从队列里取一条数据,并且关闭自动回复机制,每次取完一条数据后,手动回复并继续取下一条数据。
3.1、生产者(Send)代码
public class Send { //队列名称 private static final String QUEUE_NAME = "test_work_fair_dispatch_queue"; public static void main(String[] args) { try { //获取连接 Connection connection = ConnectionUtil.getConnection(); //从连接中获取一个通道 Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); for (int i = 0; i < 10; i++) { String message = "this is work_fair_dispatch queue message" + i; System.out.println("[send]:" + message); //发送消息 channel.basicPublish("", QUEUE_NAME, null, message.getBytes("utf-8")); Thread.sleep(20 * i); } channel.close(); connection.close(); } catch (IOException | TimeoutException | InterruptedException e) { e.printStackTrace(); } } }
运行结果:
[send]:this is work_fair_dispatch queue message0
[send]:this is work_fair_dispatch queue message1
[send]:this is work_fair_dispatch queue message2
[send]:this is work_fair_dispatch queue message3
[send]:this is work_fair_dispatch queue message4
[send]:this is work_fair_dispatch queue message5
[send]:this is work_fair_dispatch queue message6
[send]:this is work_fair_dispatch queue message7
[send]:this is work_fair_dispatch queue message8
[send]:this is work_fair_dispatch queue message9
3.2、消费者1(Receive1)代码
public class Receive1 { //队列名称 private static final String QUEUE_NAME = "test_work_fair_dispatch_queue"; public static void main(String[] args) { try { //获取连接 Connection connection = ConnectionUtil.getConnection(); //从连接中获取一个通道 final Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //设置每次从队列里取一条数据 int prefetchCount = 1; channel.basicQos(prefetchCount); //定义消费者 DefaultConsumer 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"); System.out.println("[1] Receive message:" + message); try { //消费者休息1s处理业务 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("[1] done"); //手动应答 channel.basicAck(envelope.getDeliveryTag(), false); } } }; //设置手动应答 boolean autoAck = false; //监听队列 channel.basicConsume(QUEUE_NAME, autoAck, consumer); } catch (IOException e) { e.printStackTrace(); } } }
运行结果:
[1] Receive message:this is work_fair_dispatch queue message1
[1] done
[1] Receive message:this is work_fair_dispatch queue message2
[1] done
[1] Receive message:this is work_fair_dispatch queue message4
[1] done
[1] Receive message:this is work_fair_dispatch queue message5
[1] done
[1] Receive message:this is work_fair_dispatch queue message7
[1] done
[1] Receive message:this is work_fair_dispatch queue message8
[1] done
3.3、消费者2(Receive2)代码
public class Receive2 { //队列名称 private static final String QUEUE_NAME = "test_work_fair_dispatch_queue"; public static void main(String[] args) { try { //获取连接 Connection connection = ConnectionUtil.getConnection(); //从连接中获取一个通道 final Channel channel = connection.createChannel(); //声明队列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); //保证一次只分发一个 int prefetchCount = 1; channel.basicQos(prefetchCount); //定义消费者 DefaultConsumer 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"); System.out.println("[2] Receive message:" + message); try { //消费者休息2s处理业务 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("[2] done"); //手动应答 channel.basicAck(envelope.getDeliveryTag(), false); } } }; //设置手动应答 boolean autoAck = false; //监听队列 channel.basicConsume(QUEUE_NAME, autoAck, consumer); } catch (IOException e) { e.printStackTrace(); } } }
运行结果:
[2] Receive message:this is work_fair_dispatch queue message0
[2] done
[2] Receive message:this is work_fair_dispatch queue message3
[2] done
[2] Receive message:this is work_fair_dispatch queue message6
[2] done
[2] Receive message:this is work_fair_dispatch queue message9
[2] done
总结:消费者1处理了6条数据,消费者2处理了4条数据
与轮询分发不同的是,当每个消费都设置了每次只会从队列取一条数据时,并且关闭自动应答,在每次处理完数据后手动给队列发送确认收到数据。这样队列就会公平给每个消息费者发送数据,消费一条再发第二条,而且可以在管理界面中看到数据是一条条随着消费者消费完从而减少的,并不是一下子全部分发完了。显然公平分发更符合系统设计。
注意:本文仅代表个人理解和看法哟!和本人所在公司和团体无任何关系!