4.RabbitMQ Work Queues

在Hello World中,我们用一个生产者,一个队列,一个消费者初步认识了RabbitMQ的基本使用。但在实际使用中,通常压力主要都在消费者,就像老板安排一个任务,可能只要几句话,但是员工干起活来却不是几句话能搞定的。所以我们需要引入多个消费者,大家分工合作,共同把活干完。如下图:


4.RabbitMQ Work Queues_第1张图片
Work Queues

1. 轮询分发(round-robin)

引入多个消费者也比较简单,其实只要让两个消费者都监听同一个队列即可。这里我们定义了两个消费者,其中消费者2处理消息的速度更快,那么它将会处理更多的消息吗?
生产者

public class Send {
    private static String QUEUE_NAME="test_work_queue";
    
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        for(int i = 0;i<10;i++){
            String msg = "hello"+i;
            System.out.println("生产者发送:"+msg);
            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        }
        channel.close();
        connection.close();
    }
}

消费者1

public class Receive1 {
    private static String QUEUE_NAME="test_work_queue";
    
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.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 msg = new String(body,"utf-8");
                System.out.println("消费者1:"+msg);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2

public class Receive2 {
    private static String QUEUE_NAME="test_work_queue";
    
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.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 msg = new String(body,"utf-8");
                System.out.println("消费者2:"+msg);
                try {
                    //虽然消费者2处理速度更快,但是他和消费者一处理的消息个数是一样的
                    //这种方式叫做轮询分发,任务消息总是你一个我一个
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //监听队列
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

通过测试,我们发现,两个消费者在交替处理任务,这就是轮询分发,RabbitMQ总是循环向消费者发送消息,而不管消费者的处理能力。

Console1------>
消费者1:hello0
消费者1:hello2
消费者1:hello4
消费者1:hello6
消费者1:hello8

Console2------>
消费者2:hello1
消费者2:hello3
消费者2:hello5
消费者2:hello7
消费者2:hello9

2.消息确认(Message acknowledgment)

RabbitMQ将消息投递给消费者后,它怎么判断本次消息投递成功了呢?这就要涉及到消息确认机制。

When a node delivers a message to a consumer, it has to decide whether the message should be considered handled (or at least received) by the consumer. Since multiple things (client connections, consumer apps, and so on) can fail, this decision is a data safety concern. Messaging protocols usually provide a confirmation mechanism that allows consumers to acknowledge deliveries to the node they are connected to. Whether the mechanism is used is decided at the time consumer subscribes.
Depending on the acknowledgement mode used, RabbitMQ can consider a message to be successfully delivered either immediately after it is sent out (written to a TCP socket) or when an explicit ("manual") client acknowledgement is received.

这里说的很明确,消息确认机制有两种,一种是只要消息投递(通过TCP连接发送了)就认为是成功的,另一种是需要消费者手动发送确认,而采用哪种机制是在消费者监听队列时决定的。

以我们上面的代码为例,来看消费者监听队列的最后一行代码:

channel.basicConsume(QUEUE_NAME, true, consumer);

第二个参数autoAck(automatic acknowledgement)为true,意思是自动确认,就是采用的第一种确认机制,即RabbitMQ只要投递了消息就认为是成功的,会将刚投递的消息标记为删除。这样做会有问题吗?
我们可以将生产者发送消息个数改大一点,比如30,然后,在消息处理过程中强行kill消费者1,我们将会看到,由于消费者1被终结,偶数个的消息全部丢失了,只有奇数个的消息被消费者2处理了,这显然不是我们想看到的。消费者2不是还能工作嘛,那在消费者1失联后(its channel is closed, connection is closed, or TCP connection is lost),把消息全都丢给消费者2来处理不就可以了嘛,我们需要采用第二种消息确认机制来实现这个想法。

首先将autoAck设置为false

channel.basicConsume(QUEUE_NAME, false, consumer);

然后在消息处理完成后,手动给RabbitMQ发送确认消息

channel.basicAck(envelope.getDeliveryTag(), false);

这时我们再次测试,将会发现消费者失联不会导致消息丢失了。

对于我们手动发送的确认消息,RabbitMQ会做哪些工作呢?

  1. 首先RabbitMQ会记录所有没有收到ack的消息(unacknowledged),所有的消息在收到ack之前都是unacknowledged的,unacknowledged的消息是不会被删除的,万一消费者处理发生异常,好再次投递
  2. RabbitMQ收到我们手动发送的消息确认后,会将消息删除,因为消费者只有在收到且处理完消息后才会发确认,这时就可以放心删了
  3. 在某个消费者失联后,RabbitMQ将自动把所有unacknowledged的消息重新投递给仍能正常工作的消费者,若所有消费者都失联了,消息将仍会以unacknowledged的状态存在于RabbitMQ队列中,等消费者再次连接后,再次投递

所以如果我们将autoAck设置为false,请记得一定要手动发送ack,否则RabbitMQ将在消费者断连后重新投递那些未收到ack的消息,一方面造成消息可能被重复处理,另一方面存放未收到ack的消息也会耗费内存资源。
注意这里有两个处理,未收到ack和消费者断连。
假设消费者1手动发ack,消费者2不发ack:
RabbitMQ是会继续向消费者1和消费者2投递消息的,但所有发向消费者2的消息都会被标记为unacknowledged,并存起来。注意这时奇数个的消息是发给了消费者2,也都被处理了的,只是RabbitMQ不知道而已。那unacknowledged的消息什么时候会处理呢?
一旦消费者2断连,比如我们手动kill它,RabbitMQ就会将消息投递给仍能工作的消费者1,这样就会被重复处理。
手动ack的作用是保证某一个消费者断连后,消息可以发给其他正常的消费者,而不会丢失。

3.持久化(durability)

通过手动发送消息确认,即使消费者失联消息也不会丢失,前提是RabbitMQ服务是一直运行的,否则RabbitMQ要是挂了,消息还是会丢失啊。那这个问题怎么解决呢?
我们可以将队列和消息都进行持久化处理。

  1. 队列持久化
    生产者声明队列的代码,第二个参数为durable,之前被设置为false

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

改为true即可持久化,但是我们不能直接改为true,因为指定名字的队列(test_work_queue)已经被声明为不持久化了,不能直接修改,只能重新声明一个队列(取个新的名字),或者把之前的队列删掉。

  1. 消息持久化
    生产者发送消息的代码,第三个参数properites,之前为null,现在需要修改成MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());

注意消息持久化并不能百分百保证消息一定不会丢失,因为RabbitMQ不会收到消息就立即写入磁盘,但对于一般的场景,这样做也已经能满足了,如果需要更严格的保障,可以参考publisher confirms。

4.公平分发(Fair dispatch)

轮询分发的最大问题就是不能充分利用消费者的资源,不能达到能者多劳的效果。因为RabbitMQ在消息到达队列后,就会投递,而不关心消费者unacknowledged消息数目。
我们可以使用basicQos配合prefetchCount = 1来解决这个问题。

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

它将告诉RabbitMQ,只有在收到消费者的ack之后才会向该消费者投递下一条消息,否则会投递给其他收到ack的消费者。如果所有消费者都在忙,没人返回ack,那么就只能先存到队列里了,万一队列满了,我们需要捕获该事件,然后采取一些措施,比如增加消费者等等。

来看下最终版的代码,把以下功能都集成到了一起:

  • 公平分发
  • 手动ack
  • 持久化

生产者

public class Send {
    private static String QUEUE_NAME="test_work_queue2";
    
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        //第二个参数设置为true 持久化队列
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        for(int i = 0;i<20;i++){
            String msg = "hello"+i;
            System.out.println("生产者发送:"+msg);
            //第三个参数设置为MessageProperties.PERSISTENT_TEXT_PLAIN 持久化消息
            channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
        }
        channel.close();
        connection.close();
    }
}

消费者1

public class Receive1 {
    private static String QUEUE_NAME="test_work_queue2";
    
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        //RabbitMQ在收到我反馈的ack后才投递下一条消息给我
        channel.basicQos(1);
        DefaultConsumer consumer = new DefaultConsumer(channel){
            //收到消息就会触发
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
                    throws IOException {
                String msg = new String(body,"utf-8");
                System.out.println("消费者1:"+msg);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //向生产者发送确认消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //关闭自动确认,改为手动
        boolean autoAck = false;
        //监听队列
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }
}

消费者2

public class Receive2 {
    private static String QUEUE_NAME="test_work_queue2";
    
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        //RabbitMQ在收到我反馈的ack后才投递下一条消息给我
        channel.basicQos(1);
        DefaultConsumer consumer = new DefaultConsumer(channel){
            //收到消息就会触发
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
                    throws IOException {
                String msg = new String(body,"utf-8");
                System.out.println("消费者2:"+msg);
                try {
                    //消费者2处理速度更快,由于采用了公平分发,消费者2将处理更多请求
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //向生产者发送确认消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //关闭自动确认,改为手动
        boolean autoAck = false;
        //监听队列
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }
}

你可能感兴趣的:(4.RabbitMQ Work Queues)