RabbitMQ(三)Work Queues 工作队列

1.概述

https://www.rabbitmq.com/tutorials/tutorial-two-java.html

工作队列(又名:任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反,我们安排任务在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享。

这个概念在Web应用程序中特别有用,因为在Web应用程序中,不可能在较短的HTTP请求窗口内处理复杂的任务。

2.代码示例

2.1 发送者

/**
 * 消息发送者
 */
public class Sender {
    private  final static String QUEUE = "testWork";//队列名称

    public static void main(String[] args) throws Exception {
        //1.获取连接
        Connection connection = ConnectionUtil.getConnection();
        //2.创建通道
        Channel channel = connection.createChannel();
        //3.声名队列
        //参数1:队列的名称
        //参数2:是否持久化队列,我们的队列模式是在内存中的,如果RabbitMQ重启会丢失,如果设置为true,则会保存在erlang自带的数据库中,重启后恢复
        //参数3:是否排外,有两个作用,第一个当我们的连接关闭后是否自动删除队列;第二,是否私有当前队列,如果私有,其它通道不可访问
        //参数4:是否自动删除
        //参数5:一些其它参数
        channel.queueDeclare(QUEUE,false,false,false,null);
        //4.发送消息
        for (int i = 0; i < 100; i++) {
            String msg = "这是发送的消息"+i;
            channel.basicPublish("",QUEUE,null,msg.getBytes());
        }
        //5.关闭连接
        channel.close();
        connection.close();
    }
}

2.2 消费者1号

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

import java.io.IOException;

/**
 * 消息消费者
 */
public class Recv1 {
    private  final static String QUEUE = "testWork";//队列名称

    public static void main(String[] args) throws Exception {
        //1.获取连接
        Connection connection = ConnectionUtil.getConnection();
        //2.创建通道
        Channel channel = connection.createChannel();
        //3.声名队列
        //参数1:队列的名称
        //参数2:是否持久化队列,我们的队列模式是在内存中的,如果RabbitMQ重启会丢失,如果设置为true,则会保存在erlang自带的数据库中,重启后恢复
        //参数3:是否排外,有两个作用,第一个当我们的连接关闭后是否自动删除队列;第二,是否私有当前队列,如果私有,其它通道不可访问
        //参数4:是否自动删除
        //参数5:一些其它参数
        channel.queueDeclare(QUEUE,false,false,false,null);
        //4.通知服务器,在消费确认前,不接收新发送的消息
        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));
                try {
                    Thread.sleep(10); //模拟耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //7.消费者消息确认:参数2,false 为手动确认收到消息。
                //这里可以在异常捕获的时候,将它设置为true
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        //6.注册消费者,参数2,false代表我们收到消息后需要手动告诉服务器
        channel.basicConsume(QUEUE,false,consumer);

    }
}

2.3 消费者2号

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

import java.io.IOException;

/**
 * 消息消费者
 */
public class Recv2 {
    private  final static String QUEUE = "testWork";//队列名称

    public static void main(String[] args) throws Exception {
        //1.获取连接
        Connection connection = ConnectionUtil.getConnection();
        //2.创建通道
        Channel channel = connection.createChannel();
        //3.声名队列
        //参数1:队列的名称
        //参数2:是否持久化队列,我们的队列模式是在内存中的,如果RabbitMQ重启会丢失,如果设置为true,则会保存在erlang自带的数据库中,重启后恢复
        //参数3:是否排外,有两个作用,第一个当我们的连接关闭后是否自动删除队列;第二,是否私有当前队列,如果私有,其它通道不可访问
        //参数4:是否自动删除
        //参数5:一些其它参数
        channel.queueDeclare(QUEUE,false,false,false,null);
        //4.通知服务器,在消费确认前,不接收新发送的消息
        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 String(body));
                try {
                    Thread.sleep(200); //模拟耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //7.消费者消息确认:参数2,false 为手动确认收到消息。
                //这里可以在异常捕获的时候,将它设置为true
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        //6.注册消费者,参数2,false代表我们收到消息后需要手动告诉服务器
        channel.basicConsume(QUEUE,false,consumer);
    }
}

默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。

3.消息确认

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

执行任务可能需要几秒钟。您可能想知道,如果其中一个使用者开始一项漫长的任务并仅部分完成而死掉,会发生什么情况。使用我们当前的代码,RabbitMQ一旦向消费者发送了一条消息,便立即将其标记为删除。在这种情况下,如果您杀死一个工人,我们将丢失正在处理的消息。我们还将丢失所有发送给该特定工作人员但尚未处理的消息。

但是我们不想丢失任何任务。如果一个工人死亡,我们希望将任务交付给另一个工人。

为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发送回一个确认(acknowledgement),以告知RabbitMQ已经接收,处理了特定的消息,并且RabbitMQ可以自由删除它。

如果使用者在不发送确认的情况下死亡(其通道已关闭,连接已关闭或TCP连接丢失),RabbitMQ将了解消息未完全处理,并将重新排队。如果同时有其他消费者在线,它将很快将其重新分发给另一位消费者。这样,即使工人偶尔死亡,您也可以确保不会丢失任何消息。

没有任何消息超时;消费者死亡时,RabbitMQ将重新传递消息。即使处理一条消息花费非常非常长的时间也没关系。

默认情况下,手动消息确认处于打开状态。在前面的示例中,我们通过autoAck = true标志显式关闭了它们。一旦我们完成了一项任务,就该将该标志设置为false并从工作程序发送适当的确认的时候了。

被遗忘的确认
错过basicAck是一个常见的错误。这是一个简单的错误,但是后果很严重。当您的客户端退出时,消息将被重新发送(看起来像是随机重新发送),但是RabbitMQ将消耗越来越多的内存,因为它将无法释放任何未确认的消息。为了调试这种错误,您可以使用rabbitmqctl 打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues名称messages_ready messages_unacknowledged

在Windows上,删除sudo:

rabbitmqctl.bat list_queues名称messages_ready messages_unacknowledged

4.消息持久性

boolean durable = true;
channel.queueDeclare(QUEUE, durable, false, false, null);

如果RabbitMQ服务器停止,我们的任务仍然会丢失。
RabbitMQ退出或崩溃时,它将忘记队列和消息,除非您告知不要这样做。要确保消息不会丢失,需要做两件事:我们需要将队列和消息都标记为持久性。
首先,我们需要确保该队列将在RabbitMQ节点重启后继续存在。为此,我们需要将其声明为持久的:

关于消息持久性的说明
将消息标记为持久性并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是RabbitMQ接受消息但尚未将其保存仍然有很短的时间。另外,RabbitMQ不会对每条消息都执行fsync(2)-它可能只是保存到缓存中,而没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果您需要更强有力的保证,则可以使用 发布者确认

5.公平派遣

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

您可能已经注意到,调度仍然无法完全按照我们的要求进行。例如,在有两名工人的情况下,当所有奇怪的消息都很重,甚至消息都很轻时,一位工人将一直忙碌而另一位工人将几乎不做任何工作。好吧,RabbitMQ对此一无所知,并且仍将平均分配消息。
发生这种情况是因为RabbitMQ在消息进入队列时才调度消息。它不会查看消费者的未确认消息数。它只是盲目地将每第n条消息发送给第n个使用者。


为了克服这一点,我们可以将basicQos方法与 prefetchCount = 1设置一起使用。这告诉RabbitMQ一次不要给工人一个以上的消息。换句话说,在处理并确认上一条消息之前,不要将新消息发送给工作人员。而是将其分派给尚不繁忙的下一个工作人员。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值