RabbitMq学习:工作队列


在这里插入图片描述
工作队列,用于在多个消费之间分配耗时任务,主要思想是避免立即执行资源密集型任务,而不得不等待它完成。将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当运行许多工作人员时,任务将在他们之间共享。

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

准备工作:
创建一个Send类(生产者)用于发送消息到队列中,创建一个Recv1(消费者),Recv2(消费者)用于接收消息,创建一个ConnectionUtil工具类用于获取连接。用Thread.sleep()函数来模拟接到消息后处理任务所需的时间。

工程目录:
在这里插入图片描述

任务安排者(生产者) Send.java

public class Send {
    private final static String WORK_QUEUE = "workQueue";
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(WORK_QUEUE,false,false,false,null);
        //往队列中发20条消息
        for(int i=0;i<20;i++) {
            String message = "hello"+i;
            channel.basicPublish("", WORK_QUEUE, null,message.getBytes("UTf-8") );
            System.out.println("send:"+message);
        }
        channel.close();
        connection.close();

    }

}

工人(消费者c1) Recv1.java

public class Recv1 {

    private final static String WORK_QUEUE = "workQueue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(WORK_QUEUE,false,false,false,null);
        System.out.println("recv1:waiting for meseage");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" recv1 Received '" + message + "'");
            try {
                Thread.sleep(1000);//睡眠1秒用于模仿接到消息后处理任务所需要的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally{
                System.out.println("任务完成");
            }

        };
        channel.basicConsume( WORK_QUEUE, true, deliverCallback, consumerTag -> { });


    }
}

工人(消费者c2)Recv2.java

public class Recv2 {

    private final static String WORK_QUEUE = "workQueue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(WORK_QUEUE,false,false,false,null);
        System.out.println("recv2:waiting for meseage");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" recv2 Received '" + message + "'");
            try {
                Thread.sleep(2000);//睡眠2秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally{
                System.out.println("处理任务完成");
            }
        };
        channel.basicConsume( WORK_QUEUE, true, deliverCallback, consumerTag -> { });


    }
}

消息轮询分发

启动RabbitMQ服务器,然后运行Recv1,Recv2
在这里插入图片描述
在这里插入图片描述
然后启动Send类,发送消息到队列中
在这里插入图片描述
此时,观察Recv1和Recv2接收消息的情况:
Recv1:在这里插入图片描述
Recv2:
在这里插入图片描述

可以看到,他们接收到的消息数量是一样的。默认情况下,RabbitMQ将每个消息依次发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为轮询。

消息确认

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

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

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

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

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

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

channel.basicQos(1); 

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
  String message = new String(delivery.getBody(), "UTF-8");

  System.out.println(" Received '" + message + "'");
  try {
    Thread.sleep();
  } finally {
    System.out.println(" 任务完成");
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
  }
};
boolean autoAck = false;//任务完成后手动发送回执给RabbitMQ确认
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

这样,即使某个工人在处理任务时,只处理了一部分而突然杀掉这个进程,消息也不会丢失,因为所有未确认的消息,都会重新发送给其他工人。

消息持久化

通过消息确认机制,我们可以保证在消费者挂掉的情况,不会丢失消息,但是有一种情况,如果RabbitMQ服务器挂掉了怎么办呢?消息依然会丢失,服务器退出或者崩溃,就会忘记所有的队列和信息,除非我们告知不要这样做。
要确保消息不会丢失,需要做两件事:我们需要将队列和消息都标记为持久性。

首先,我们需要确保该队列将在RabbitMQ节点重启后继续存在。为此,我们需要将其声明为持久的:

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

注意:
队列声明需要同时应用到生产者和消费者中去。RabbitMQ不允许使用不同的参数重新定义一个已有的队列,例如已经有一个队列是不持久化的,如果再重新定义它为持久性的,这时就会报错。

现在需要将消息标记为持久性-通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。

channel.basicPublish(“”,“ task_queue”,
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

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

公平派遣

根据前面的例子,调度仍然无法完全按照我们的要求进行。消费者1处理任务需要耗时1秒,消费者2处理任务需要耗时2秒,但结果是他们两接收的消息数量是一样的。结果造成一个消费者非常繁忙而另一个非常悠闲。RabbitMQ对此一无所知,并且仍将平均分配消息。

发生这种情况是因为RabbitMQ在消息进入队列时才调度消息。它不会查看消费者的未确认消息数。它只是盲目地将每第n条消息发送给第n个使用者。
在这里插入图片描述
为了解决这个问题,我们可以将basicQos方法与 prefetchCount = 1设置一起使用。这告诉RabbitMQ一次不要给工人一个以上的消息。换句话说,在处理并确认上一条消息之前,不要将新消息发送给工作人员。而是将其分派给尚不繁忙的下一个工作人员。

int prefetchCount=1
channel.basicQos(prefetchCount);

合并后代码

Recv1.java

/*
消费者1:公平模式(根据能力分发消息)
 */
public class Recv1 {
    private final static String WORK_QUEUE_FIAR = "work_queue_fair";
    public static void main(String[] args) throws IOException, TimeoutException {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //声明队列
        channel.queueDeclare(WORK_QUEUE_FIAR,true,false,false,null);
        System.out.println("recv1:waiting message...");
        int prefetch=1;
        channel.basicQos(prefetch);
        DeliverCallback deliverCallback = (consumetTag,deliver)->{
            String message = new String(deliver.getBody(),"UTf-8");
            System.out.println("recv1: receive '"+message+"'");
            try{
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                //消息确认,告诉RabbitMQ已经处理往完这个消息
                channel.basicAck(deliver.getEnvelope().getDeliveryTag(),false);
            }
        };
        boolean autoAck = false;    //开启手动确认消息
        channel.basicConsume(WORK_QUEUE_FIAR,autoAck,deliverCallback,consumerTag->{});
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值