RabbitMQ之Work Queues模式

RabbitMQ之Work Queues模式

本下面的文字代码原来自官网👉 附上链接–》RabbitMq 之 Work Queues
看完这篇文章对你绝对有好处。
好处一、你可以了解透 Work Queues模式,本文章内容98%以上都是来自官网。通俗易懂。
好处二、看完文章,你再点击上面的官网连接,进去看一看官网的文档,你会发现读官方文档不是难事。
😀

先附上所有的模式
在这里插入图片描述
在这里插入图片描述

当前文章所讲的模式对应下图
在这里插入图片描述
注意:下文说的 “消息"与"任务”、“woker"与"消费者” 可以简单理解为同一个东西。
在上一次中,我们通过命名队列进行发送和接受消息。在这一次中在我们将创建一个工作队列,用于分发耗时的任务在多个wokers(这里的wokers可以理解为消费同一队列中的消息的多个消费者)

意义:背后的主要思想 工作队列(又名:任务队列)是为了避免立即做一个资源密集型任务,不得不等待它完成。相反,我们安排以后的任务要做。我们封装任务作为消息并将其发送到一个队列。一个消费者woker在后台运行时将会从任务队列中取出任务并去完成该任务。当有多个woker进程时,任务队列中的任务将会被这些woker共享。

 通俗的理解就是,在之前的代码中,一个任务队列只有一个woker从其取出任务去消费。当我们工作队列中有多个任务,且希望这些任务能够尽快被消费掉,那么此时就可以让多个woker来处理一个任务队列中的多个任务,人多力量大。

准备

发送端:

public class NewTask{
  private final static String QUEUE_NAME = "hello";
  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
	factory.setHost("localhost");
	try (Connection connection = factory.newConnection();
	    Channel channel = connection.createChannel()) {
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);
		String message = String.join(" ", argv); //argv参数在运行时传入
		channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
		System.out.println(" [x] Sent '" + message + "'");
	}
  }
}

接收端:

public class Worker{

  private final static String QUEUE_NAME = "hello";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
	DeliverCallback deliverCallback = (consumerTag, delivery) -> {
	  String message = new String(delivery.getBody(), "UTF-8");
	  System.out.println(" [x] Received '" + message + "'");
	  try {
	    doWork(message);
	  } finally {
	    System.out.println(" [x] Done");
	  }
	};
	boolean autoAck = true; // acknowledgment is covered below
	channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
  }
  //通过doWork用于模拟各个任务不同的所需消费时间
  private static void doWork(String task) throws InterruptedException {
    for (char ch: task.toCharArray()) {
        if (ch == '.') Thread.sleep(1000);
    }
  }
}

编译这两个java文件

javac -cp $CP NewTask.java Worker.java

Round-robin dispatching(轮询转发)

使用一个任务队列的优点之一是能够实现并行消费工作。如果我们堆积了很多任务需要被消费者woker消费,我们可以添加更多的woker加速消费众多的任务。

首先,启动两个消费同一任务队列的woker,让它们阻塞等待接收消息。

# shell 1
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# shell 2
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C

然后启动生产者,用于发送消息

# shell 3
java -cp $CP NewTask First message.
# => [x] Sent 'First message.'
java -cp $CP NewTask Second message..
# => [x] Sent 'Second message..'
java -cp $CP NewTask Third message...
# => [x] Sent 'Third message...'
java -cp $CP NewTask Fourth message....
# => [x] Sent 'Fourth message....'
java -cp $CP NewTask Fifth message.....
# => [x] Sent 'Fifth message.....'

观察效果

java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'
java -cp $CP Worker
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

 你是否发现,无论每个任务所需消费的时间是否相同,RabbitMQ是轮询地把任务转发给woker进行消费。

 默认RabbitMQ 会把每个消息按顺序发给下一个消费者(woker),平均每个消费者能拿到相同数量的消息(任务),这种方式称为round-robin(轮询)

Message acknowledgment(消息确认)

 执行一个任务需要花费少量时间。那当消费者执行一个花费时间较长的任务,执行了很久,且执行了一部分,此时消费者就挂了,那么此时会发生什么情况?
 以上面代码为例,一旦RabbitMQ把消息推送给消费者后,马上就删除掉该消息。所以在当前的情况下,如果当前消费者正在处理消息的过程中,你就把当前的消费者给杀死,那么此时我们就会丢失给该消费者所发送的所有消息,这些消息又可能是该消费者还未处理完的,此时就会引发消息丢失问题
  如果我们不想要消息丢失,一旦消费者挂掉,我们希望把当前消费者的消息退回到队列中,以让其它活着的消费者进行消费。
 为了不让消息丢失,那么让消费者 消费完成后发送一个消息确认(message acknowledgement)给RabbitMQ,让RabiitMQ知道当前消息已经确实是被消费者接受且被处理完了,此时RabbitMQ就可以放心安全地删除该消息了。

 如果消费者挂了(如channel信道关闭,connection连接关闭、TCPconnection 连接丢失),而没有发送 ACK消息确认,RabbitMQ就为认为该消息没有被消费者完全地消费掉,然后就把该消息重新退回到消息(任务)队列中,如果此时存在其它在线的消费者,那么该消息就可以快速地被其它消费者给消费掉

消息确认没有超时时间限制只有当消费者挂了(如channel信道关闭,connection连接关闭、TCPconnection 连接丢失),消息才会被退回到消息队列,反之是不会将消息退回到消息队列。如果消费者是正常状态,哪怕该消息被消费者处理了很长很长时间,一直没给RabbitMQ发送ack确认,也是没有问题的
Manual message acknowledgments手动消息确认 默认是打开的。但是在上面的代码案例中,是显示关闭掉手动确认的,即autoAck=true。我们可以把autoAck置为false,来实现非自动ACK确认。
:这里的手动消息确认的手动是指消息处理完成后,调用一个方法进行主动确认

channel.basicQos(1); // accept only one unack-ed message at a time (see below)

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

  System.out.println(" [x] Received '" + message + "'");
  try {
    doWork(message);
  } finally {
    System.out.println(" [x] Done");
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
  }
};
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

通过上面代码,哪怕消费者还未把消息消费掉,你就通过消费者给杀死,没有消息会丢失消费者死亡一会后,RabbitMQ就会察觉到该消费者挂了所有给该消费者且还未ACK的消息,都会被退回到消息队列中

ACK确认与消息接收必须使用相同的channel信道。俗话就是 这个消息是哪个消息者消费的,就必须由该消费者进行ACK确认。如果没有遵循该规则,如果接受消息和进行该消息的Ack是不同信道,那么就会出现 channel-level protocol exception。

Forgotten acknowledgment
 忘记basicAck进行主动确认是很普遍的错误。该错误很容易发生,但是其导致的后果却是非常严重的。
问题一、消息被重复消费问题。当你消费完没有确认,当你关闭消费者所在的应用后,消息会被回退到消息队列,导致被重复消费。
问题二、吃系统内存。因为一旦你没有主动ACK确认,那么消息就会一直停留在RabbitMQ(内存)上,消息没有被释放掉,然后就不断得积累,不断得吃系统内存。

Message durability(消息持久化)

 我们已经学了当消费者挂掉后,如何让消息不会丢失。但是当RabbitMQ服务停止后,消息依然回被丢失。
如果你不告诉RabiitMQ该怎么做,当RabbitMQ停止或者宕机后,所有的队列(Queues)和消息(Message)都会被丢失。所以为了确保两者的数据都不会丢失,那么我们需要将消息和队列持久化

首先,我们为了确保当RabbitMQ重启后,队列queue不会丢失,那么需要把该队列声明成durable持久化。

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

 尽管上面的写法是正确的,但是上面的最新配置不会生效,因为再此之前我们已经声明过了一个名为“hello”的队列了。RabbitMQ不允许我们对一个队列定义两种不同的参数配置。你可以在RabbitMQ管理界面上将原来的队列修改成持久化的。如果不想,可以变通一点我们可以把上面队列名修改成RabbitMQ中没有的队列名即可,如task_queue

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

queueDeclare中的改变,需要应用在生产者和消费者的queueDeclare函数。

 当前,即使RabbitMQ服务重启,task_queue队列已经不会丢失。此时我们希望消息Message也能像队列一样持久化,那么我们的生产者可以设置MessageProperties(实现了BasicProperties) 的值为PERSISTENT_TEXT_PLAIN,以实现对消息进行持久化。

import com.rabbitmq.client.MessageProperties;

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

Note on message persistence
Marking messages as persistent doesn’t
fully guarantee that a message won’t be lost. Although it tells
RabbitMQ to save the message to disk, there is still a short time
window when RabbitMQ has accepted a message and hasn’t saved it yet.
Also, RabbitMQ doesn’t do fsync(2) for every message – it may be just
saved to cache and not really written to the disk. The persistence
guarantees aren’t strong, but it’s more than enough for our simple
task queue. If you need a stronger guarantee then you can use
publisher confirms.

Fair dispatch(公平转发)

你可能会注意到消息转发没有你想象的那么完美。
 例如:假设当前由两个woker消费者,队列中奇数号的消息内容非常复杂,消费者处理需要花费很长时间,又因为消息是轮询转发的,所以处理奇数号的woker一直是同一个woker,所以此时处理奇数的worker一直处于忙碌状态,甚至处理不过来,出现消息堆积,而另外一个woker很轻松,甚至没什么活干。但是RabbitMQ不知道一个消费者很忙碌,一个消费者很轻松,所以一直是轮询转发。

 这种问题的产生是因为RabbitMQ接收到一个消息之后,不管某个消费者有没有把之前的消息给消费完,不管某个消费者是不是还存在大量的消息没有ACK,RabbitMQ都会按轮询方式立即转发给消费者。
在这里插入图片描述
 为了解决上面的问题,我们可以使用basicQos 方法,同时方法参数值设置为 prefetchCount = 1,这样就可以告诉RabbitMQ一个不要给消费者超过一个的消息。换句话说,如果消费者还未将上次所给的消息进行ACK,就不要把新的消息转发给消费者,直到消费者对上次拿到的消息进行ACK。

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

队列大小的问题
如果所有的消费者woker很忙,您的队列可以填满。你要留意,或者添加更多的worker,或者有其他策略。

上述的最终代码整合

生产者

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

public class NewTask {

  private static final String TASK_QUEUE_NAME = "task_queue";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

        String message = String.join(" ", argv);

        channel.basicPublish("", TASK_QUEUE_NAME,
                MessageProperties.PERSISTENT_TEXT_PLAIN,
                message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + message + "'");
    }
  }

}

消费者

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

public class Worker {

  private static final String TASK_QUEUE_NAME = "task_queue";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    final Connection connection = factory.newConnection();
    final Channel channel = connection.createChannel();

    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

    channel.basicQos(1);

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

        System.out.println(" [x] Received '" + message + "'");
        try {
            doWork(message);
        } finally {
            System.out.println(" [x] Done");
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    };
    channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
  }

  private static void doWork(String task) {
    for (char ch : task.toCharArray()) {
        if (ch == '.') {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException _ignored) {
                Thread.currentThread().interrupt();
            }
        }
    }
  }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值