RabbitMQ 工作队列

本文介绍了RabbitMQ中的工作队列概念,包括轮询分发和公平分发两种方式。在轮询分发中,消息均匀分配给多个消费者,而公平分发则根据消费者处理速度动态调整。文中通过Java代码示例展示了这两种分发方式的实现,并讨论了启动消费者和生产者的时间顺序对消息处理的影响。
一、工作队列

官方文档:http://www.rabbitmq.com/tutorials/tutorial-two-java.html

简单队列不足:不支持多个消费者

即一个生产者可以对应多个消费者同时消费,相比简单队列支持多消费者。因为实际工作中,生产者服务一般都是很简单的业务逻辑处理之后就发送到队列, 消费者接收到队列的消息之后,进行复杂的业务逻辑处理,所以一般都是多个消费者进行处理。如果是一个消费者进行处理,那么队列会积压很多消息。

工作队列分两种情况:

轮询分发

不管消费者处理速度性能快慢,每个消费者都是按顺序分别每个拿一个的原则,比如3个消费者, 
消费者1拿1个,然后消费者2拿一个,然后消费者3拿一个,然后消费者1开始拿,即使中间有消费
者已经处理完了,也必须等待其他消费者都拿完一个才能消费到。

公平分发

根据消费者处理性能,性能好的消费的数据量多,性能差的消费的数据量少。

如上所示,如果配置有用户名密码以及vhost,则配置即可。
二、轮询分发
一个生产者,两个消费者,其中消费者处理只要1s,消费者2 处理需要2s
生产者
package com.rabbitmq.work;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 生产者
 */
public class Send {

    private static final String QUEUE_NAME = "test_work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 从连接开一个通道
        Channel channel = connection.createChannel();
        // 申明这个通道连接的队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        for (int i = 0; i < 20; i++) {
            String message = "Hello RabbitMQ " + i;
            // 发送消息
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }

        // 关闭通道和连接
        channel.close();
        connection.close();
    }

}

消费者1
package com.rabbitmq.work;

import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者
 */
public class Recv1 {

    private static final String QUEUE_NAME = "test_work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 获取连接
        Connection connection = ConnectionUtil.getConnection();

        // 打开通道
        Channel channel = connection.createChannel();

        // 申明要消费的队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 创建一个回调的消费者处理类
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 接收到的消息
                String message = new String(body);
                System.out.println(" [1] Received '" + message + "'");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(" [1] done ");
                }
            }
        };

        // 消费消息
        channel.basicConsume(QUEUE_NAME, true, consumer);

    }

}

消费者2
package com.rabbitmq.work;

import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者
 */
public class Recv2 {

    private static final String QUEUE_NAME = "test_work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 获取连接
        Connection connection = ConnectionUtil.getConnection();

        // 打开通道
        Channel channel = connection.createChannel();

        // 申明要消费的队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 创建一个回调的消费者处理类
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 接收到的消息
                String message = new String(body);
                System.out.println(" [2] Received '" + message + "'");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(" [2] done ");
                }
            }
        };

        // 消费消息
        channel.basicConsume(QUEUE_NAME, true, consumer);

    }

}

测试
  1. 先启动两个消费者Recv、Recv2

  1. 再启动生产者Send

生产者控制台:

 [x] Sent 'Hello RabbitMQ 0'
 [x] Sent 'Hello RabbitMQ 1'
 [x] Sent 'Hello RabbitMQ 2'
 [x] Sent 'Hello RabbitMQ 3'
 [x] Sent 'Hello RabbitMQ 4'
 [x] Sent 'Hello RabbitMQ 5'
 [x] Sent 'Hello RabbitMQ 6'
 [x] Sent 'Hello RabbitMQ 7'
 [x] Sent 'Hello RabbitMQ 8'
 [x] Sent 'Hello RabbitMQ 9'
 [x] Sent 'Hello RabbitMQ 10'
 [x] Sent 'Hello RabbitMQ 11'
 [x] Sent 'Hello RabbitMQ 12'
 [x] Sent 'Hello RabbitMQ 13'
 [x] Sent 'Hello RabbitMQ 14'
 [x] Sent 'Hello RabbitMQ 15'
 [x] Sent 'Hello RabbitMQ 16'
 [x] Sent 'Hello RabbitMQ 17'
 [x] Sent 'Hello RabbitMQ 18'
 [x] Sent 'Hello RabbitMQ 19'

Process finished with exit code 0

消费者1:

 [1] Received 'Hello RabbitMQ 0'
 [1] done 
 [1] Received 'Hello RabbitMQ 2'
 [1] done 
 [1] Received 'Hello RabbitMQ 4'
 [1] done 
 [1] Received 'Hello RabbitMQ 6'
 [1] done 
 [1] Received 'Hello RabbitMQ 8'
 [1] done 
 [1] Received 'Hello RabbitMQ 10'
 [1] done 
 [1] Received 'Hello RabbitMQ 12'
 [1] done 
 [1] Received 'Hello RabbitMQ 14'
 [1] done 
 [1] Received 'Hello RabbitMQ 16'
 [1] done 
 [1] Received 'Hello RabbitMQ 18'
 [1] done 

消费者2:

 [2] Received 'Hello RabbitMQ 1'
 [2] done 
 [2] Received 'Hello RabbitMQ 3'
 [2] done 
 [2] Received 'Hello RabbitMQ 5'
 [2] done 
 [2] Received 'Hello RabbitMQ 7'
 [2] done 
 [2] Received 'Hello RabbitMQ 9'
 [2] done 
 [2] Received 'Hello RabbitMQ 11'
 [2] done 
 [2] Received 'Hello RabbitMQ 13'
 [2] done 
 [2] Received 'Hello RabbitMQ 15'
 [2] done 
 [2] Received 'Hello RabbitMQ 17'
 [2] done 
 [2] Received 'Hello RabbitMQ 19'
 [2] done 

可以发现消费者1 消费的数字全是整数,消费者2消费的全是奇数。

那么如果我事先启动三个消费者了,那么结果如何了?

因为20/3 除不了,如果消费者1,2,3按顺序启动,那么消费者1, 2会消费7条数据,消费者3消费6条,其中

消费者1 消费 0, 3, 6, 9, 12, 15, 18
消费者2 消费 1, 4, 7, 10, 13, 16, 19
消费者3 消费 2, 5, 8, 11, 14, 17

注意点:

如果生产者一次性发送完消息之后,再依次启动消费者1, 2, 3, 之后只有消费者1 能消费到数据,消费者都启动之后,再生产的消息就会轮询分发到消费者1, 2, 3

三、公平分发

官方示例图:

因为生产者发送消息到队列之后,队列不知道消费者有没有处理完,所以多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。为了实现公平分发,我们需要告诉队列,每次发一个给我,然后我反馈给你我有没有处理完,处理完了你再发一条给我。

在默认轮询分发的基础上,要实现公平分发,需要两点:

限制发送给同一个消费者不得超过1条消息,在这个消费者确认消息之前,不会发送下一条消息给这个消费者

int prefetchCount = 1;
channel.basicQos(prefetchCount);

默认自动应答改成手动应答


关闭自动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

手动应答
channel.basicAck(envelope.getDeliveryTag(), false);

DeliveryTag 用来标识信道中投递的消息, RabbitMQ 推送消息给 Consumer 时,会附带一个 Delivery Tag,以便 Consumer 可以在消息确认时告诉 RabbitMQ 到底是哪条消息被确认了。
生产者
package com.rabbitmq.workfair;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 生产者
 */
public class Send {

    private static final String QUEUE_NAME = "test_workfair_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 从连接开一个通道
        Channel channel = connection.createChannel();
        // 申明这个通道连接的队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //服务器将传递的最大消息数,如果无限制则为 0
        channel.basicQos(1);

        for (int i = 0; i < 20; i++) {
            String message = "Hello RabbitMQ " + i;
            // 发送消息
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
            try {
                Thread.sleep(i*100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 关闭通道和连接
        channel.close();
        connection.close();
    }

}

消费者1
package com.rabbitmq.workfair;

import com.rabbitmq.client.*;
import com.rabbitmq.util.ConnectionUtil;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 消费者
 */
public class Recv1 {

    private static final String QUEUE_NAME = "test_workfair_queue";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 获取连接
        Connection connection = ConnectionUtil.getConnection();

        // 打开通道
        final Channel channel = connection.createChannel();

        // 申明要消费的队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。
        // 换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。
        channel.basicQos(1);

        // 创建一个回调的消费者处理类
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 接收到的消息
                String message = new String(body);
                System.out.println(" [1] Received '" + message + "'");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(" [1] done ");
                    //手动应答:当处理完消息回复服务器
                    channel.basicAck(envelope.getDeliveryTag(), false);
                    //multiple – true 以确认所有消息,包括提供的交付标签; false 仅确认提供的交付标签。
                    //void basicAck(long deliveryTag, boolean multiple) throws IOException;

                }
            }
        };

        //关闭自动应答
        boolean autoAck = false;
        // 消费消息
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }

}

消费者2
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑石课堂

请给我打钱!!!谢谢,不客气!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值