四RabbitMQ中模式—工作队列(Work Queues)模式

Posted 上善若水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了四RabbitMQ中模式—工作队列(Work Queues)模式相关的知识,希望对你有一定的参考价值。

一、工作队列(work queues)模式

工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作经常将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。

1.1、轮询分发消息

在这个案例中我们会启动两个工作线程,一个是消息发送线程,我们来看看它们两个工作线程是如何工作的。

1.1.1、抽取工具类

package com.xbmu.rabbitmq.utils;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class RabbitMqUtils {

    // 得到一个连接的 channel
    public static Channel getChannel() throws Exception{
        // 创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.184.110");
        factory.setUsername("admin");
        factory.setPassword("123");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        return channel;
    }
}

1.1.2、启动一个发送线程(生产者)

package com.xbmu.rabbitmq.two;

import com.rabbitmq.client.Channel;
import com.xbmu.rabbitmq.utils.RabbitMqUtils;

import java.util.Scanner;

public class Task01 {
    private static final String QUEUE_NAME="work_queue";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        // 从控制台中接受信息
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            /**
             * void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;
             * 1.发送到哪个交换机
             * 2.路由的key值是哪个  本次是队列的名称
             * 3.其他参数消息
             * 4.发送消息的消息体
             */
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println("发送消息完成:"+message);
        }
    }
}

1.1.3、启动两个工作线程(消费者)

package com.xbmu.rabbitmq.two;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.xbmu.rabbitmq.utils.RabbitMqUtils;

public class Worker1 {
    public static final String QUEUE_NAME = "work_queue";
    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            System.out.println("接收到的消息:"+new String(delivery.getBody()));

        };
        // 消息接收被取消时,执行下面内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消息接口回调逻辑");
        };
        System.out.println("C2等待接收消息...");
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}


1.1.4、结果演示

通过程序执行发现生产者总共发送4个消息,消费者1和消费者2分别分得两个消息,并且是按照有序的一个接收一个消息
生产者

消费者1

消费者2

1.2、消息应答

1.2.1、概念

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有了消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,rabbitmq引入了消息应答机制,消息应带就是:
消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除了。

1.2.2、自动应答

消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channel关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅使用在消费者可以高效并以某种效率能够处理这些消息的情况下使用

1.2.3、消息应答的方法

1.2.4、Multiple的解释

手动应答的好处是可以批量应答并且减少网络拥堵

multiple 的 true 和 false 代表不同意思
	true 代表批量应答 channel 上未应答的消息
	比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时5-8 的这些还未应答的消息都会被确认收到消息应答
	
	false 同上面相比
	只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答

1.2.5、消息自动重新入队

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息未完成处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。

1.2.6、消息手动应答代码

默认消息采用的是自动应答,所以我们要想实现消息消费过程不丢失,需要把自动应答改为手动应答,消费者在上面的代码的基础上增加下面画红色部分的代码。

工具类

package com.xbmu.rabbitmq.utils;

/**
 * 睡眠工具类
 */
public class SleepUtils {
    public static void sleep(int second){
        try {
            Thread.sleep(1000*second);
        } catch (InterruptedException _ignored) {
            Thread.currentThread().interrupt();
        }
    }
}

消息生产者

package com.xbmu.rabbitmq.three;

import com.rabbitmq.client.Channel;
import com.xbmu.rabbitmq.utils.RabbitMqUtils;

import java.util.Scanner;

public class Task02 {
    private static final String QUEUE_NAME="ack_queue";
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        // 从控制台中接受信息
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入消息:");
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
            System.out.println("生产者发出消息:"+message);
        }
    }
}

消费者01

package com.xbmu.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.xbmu.rabbitmq.utils.RabbitMqUtils;
import com.xbmu.rabbitmq.utils.SleepUtils;

public class Worker2 {
    public static final String QUEUE_NAME = "ack_queue";
    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1等待接收消息处理时间较短");
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            // 沉睡1秒
            SleepUtils.sleep(1);
            System.out.println("接收到的消息:"+new String(delivery.getBody(),"UTF-8"));
            // 手动应答
            /**
             * 1.消息的标记 tag
             * 2.是否批量应答  false:不批量应答信道中的消息  true:批量
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

        };
        // 消息接收被取消时,执行下面内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消息接口回调逻辑");
        };
        // 采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}

消费者02

package com.xbmu.rabbitmq.three;

import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.xbmu.rabbitmq.utils.RabbitMqUtils;
import com.xbmu.rabbitmq.utils.SleepUtils;

public class Worker3 {
    public static final String QUEUE_NAME = "ack_queue";
    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2等待接收消息处理时间较长");
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            // 沉睡1秒
            SleepUtils.sleep(30);
            System.out.println("接收到的消息:"+new String(delivery.getBody(),"UTF-8"));
            // 手动应答
            /**
             * 1.消息的标记 tag
             * 2.是否批量应答  false:不批量应答信道中的消息  true:批量
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);

        };
        // 消息接收被取消时,执行下面内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消息接口回调逻辑");
        };
        // 采用手动应答
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
    }
}

1.2.7、手动应答效果演示

正常情况下消息发送方发送两个消息C1和C2,分别接收到消息并进行处理。

在发送者发送消息dd,发出消息之后把C2消费者停掉,按理说该C2来处理该消息,但是由于它处理时间较长,再还未处理完,也就是说C2还没有执行ack代码的时候,C2被停掉了,此时会看到消息被C1接收到了,说明消息dd被重新入队,然后分配给能处理消息的C1处理了。

当发送者发送方消息dd后,将消费者C2停掉后,从rabbitmq控制台可以看到:

然后,消息dd重新入队后,就会被消费者C1消费的,从rabbitmq控制台可以看到:

以上是关于四RabbitMQ中模式—工作队列(Work Queues)模式的主要内容,如果未能解决你的问题,请参考以下文章

RabbitMQ Work Queues(工作队列)

RabbitMQ --- Work Queues(工作队列)

RabbitMQ的相关概念及队列模式

RabbitMQ的相关概念及队列模式

RabbitMQ的相关概念及队列模式

RabbitMQ的相关概念及队列模式