RabbitMQ使用教程(官网翻译)

介绍

RabbitMQ是消息代理:它接受并转发消息。您可以将其视为邮局:将您要发布的邮件放在邮箱中时,可以确保Mailperson先生或女士最终将邮件传递给收件人。以此类推,RabbitMQ是一个邮箱,一个邮局和一个邮递员。

RabbitMQ与邮局之间的主要区别在于,它不处理纸张,而是接收,存储和转发数据消息的二进制斑点。

1. 基本队列(Hello World)

做某事 的最简单的事情

生产者

 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.Channel;
 public class Send {
   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 = "Hello World!";
  channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
  System.out.println(" [x] Sent '" + message + "'");
  }
  }
 }    

 

消费者

 public class Recv {
 
   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 + "'");
 };
 channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
       
       
  }
 }

2. 工作队列(Work queues)

在工人之间分配任务(竞争的消费者模式

在第一个教程中,我们编写了程序来从命名队列发送和接收消息。在这一节中,我们将创建一个工作队列,该队列将用于在多个工作人员之间分配耗时的任务。

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

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

准备

在本教程的上半部分,我们发送了一条包含“ Hello World!”的消息。现在,我们将发送代表复杂任务的字符串。我们没有现实世界中的任务,例如要调整大小的图像或要渲染的pdf文件,所以我们假装自己很忙-使用Thread.sleep()函数来伪造它。我们将字符串中的点数作为它的复杂度。每个点将占“工作”的一秒钟。例如,Hello ...描述的虚假任务 将花费三秒钟。

我们将略微修改上一个示例中的Send.java代码,以允许从命令行发送任意消息。该程序会将任务安排到我们的工作队列中,因此将其命名为 NewTask.java:

生产者

 package cn.jjaxll.amqp.workqueue;
 
 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.Channel;
 public class NewTask {
   private final static String TASK_QUEUE_NAME = "hello";
   public static void main(String[] argv) throws Exception {
      ConnectionFactory factory = new ConnectionFactory();
  factory.setHost("192.168.123.123");
  try (Connection connection = factory.newConnection();
      Channel channel = connection.createChannel()) {
 
     channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
          for (int i = 0; i < 100; i++) {
              // 发送消息
              String message = String.join(" ", "Task"+i);
              channel.basicPublish("", "hello", null, message.getBytes());
              System.out.println(" [x] Sent '" + message + "'");
          }
 
  }
  }
 }    

消费者

 package cn.jjaxll.amqp.workqueue;
 
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.DeliverCallback;
 
 public class Work {
 
   private final static String TASK_QUEUE_NAME = "hello";
 
   public static void main(String[] argv) throws Exception {
     ConnectionFactory factory = new ConnectionFactory();
     factory.setHost("192.168.123.123");
     Connection connection = factory.newConnection();
     Channel channel = connection.createChannel();
 
     channel.queueDeclare(TASK_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);
  } catch (InterruptedException e) {
     e.printStackTrace();
  } finally {
     System.out.println(" [x] Done");
  }
 };
 boolean autoAck = true; // acknowledgment is covered below
 channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });
 };
       
 
 private static void doWork(String task) throws InterruptedException {
     for (char ch: task.toCharArray()) {
         if (ch == '.') Thread.sleep(1000);
    }
 }
 }

循环调度

使用任务队列的优点之一是能够轻松并行化工作。如果我们正在积压工作,我们可以增加更多的工人,这样就可以轻松扩展。

首先,让我们尝试同时运行两个辅助实例。他们都将从队列中获取消息,但是究竟如何呢?让我们来看看。

您需要打开三个控制台。两个将运行消费者程序。。

image-20201214113451923

image-20201214113458685

image-20201214113504566

默认情况下,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(" [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服务器停止,我们的任务仍然会丢失。

RabbitMQ退出或崩溃时,除非您告诉它,否则它将忘记队列和消息。要确保消息不会丢失,需要做两件事:我们需要将队列和消息都标记为持久。

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

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

尽管此命令本身是正确的,但在我们当前的设置中将无法使用。那是因为我们已经定义了一个叫hello的队列 ,它并不持久。RabbitMQ不允许您使用不同的参数重新定义现有队列,并且将向尝试执行此操作的任何程序返回错误。但是有一个快速的解决方法-让我们声明一个名称不同的队列,例如task_queue:

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

此queueDeclare更改需要同时应用于生产者代码和使用者代码。

在这一点上,我们确保即使RabbitMQ重新启动,task_queue队列也不会丢失。现在,我们需要将消息标记为持久性-通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。

 import com.rabbitmq.client.MessageProperties;
 
 channel.basicPublish("", "task_queue",
             MessageProperties.PERSISTENT_TEXT_PLAIN,
             message.getBytes());

有关消息持久性的说明

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

公平派遣

您可能已经注意到,调度仍然无法完全按照我们的要求进行。例如,在有两名工人的情况下,当所有奇怪的消息都很重,甚至消息很轻时,一位工人将一直忙碌而另一位工人将几乎不做任何工作。好吧,RabbitMQ对此一无所知,并且仍将平均分派消息。

发生这种情况是因为RabbitMQ在消息进入队列时才调度消息。它不会查看消费者的未确认消息数。它只是盲目地将每第n条消息发送给第n个使用者。

img

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

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

关于队列大小的注意

如果所有工作人员都忙,则您的队列可以填满。您将需要关注这一点,并可能会增加更多的工作人员,或者有其他一些策略。

全部放在一起

NewTask.java类的最终代码:

 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 + "'");
    }
  }
 
 }

还有我们的Worker.java:

 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();
            }
        }
    }
  }
 }

使用消息确认和prefetchCount可以设置工作队列。耐用性选项即使重新启动RabbitMQ也能使任务继续存在。

有关Channel方法和MessageProperty的更多信息,您可以在线浏览 JavaDocs

现在,我们可以继续进行教程3,并学习如何将相同的消息传递给许多消费者。

生产非适用性免责声明

请记住,本教程和其他教程都是教程。它们一次展示一个新概念,并且可能有意过分简化了某些事情,而忽略了其他事情。例如,为简洁起见,很大程度上省略了诸如连接管理,错误处理,连接恢复,并发和度量收集之类的主题。此类简化的代码不应视为已准备就绪。

在使用您的应用程序之前,请先阅读其余文档。我们特别推荐以下指南:发布者确认和消费者确认生产清单监控

3. 发布/订阅(Publish/Subscribe)

一次向许多消费者发送消息

在上一个教程中,我们创建了一个工作队列。工作队列背后的假设是,每个任务都恰好交付给一个工人。在这一部分中,我们将做一些完全不同的事情-我们将消息传达给多个消费者。这种模式称为“发布/订阅”。

为了说明这种s模式,我们将构建一个简单的日志记录系统。它由两个程序组成-第一个程序将发出日志消息,第二个程序将接收并打印它们。

在我们的日志系统中,接收器程序的每个运行副本都将获取消息。这样,我们将能够运行一个接收器并将日志定向到磁盘。同时我们将能够运行其他接收器并在屏幕上查看日志。

本质上,已发布的日志消息将被广播到所有接收者。

交流交流

在本教程的前面部分中,我们向队列发送消息和从队列接收消息。现在是时候在Rabbit中引入完整的消息传递模型了。

让我们快速回顾一下先前教程中介绍的内容:

  • 生产者是发送消息的用户的应用程序。

  • 队列是一个缓冲区,用于存储消息。

  • 消费者是接收消息的用户的应用程序。

RabbitMQ消息传递模型中的核心思想是生产者从不将任何消息直接发送到队列。实际上,生产者经常甚至根本不知道是否将消息传递到任何队列。

相反,生产者只能将消息发送到交换机。交流是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面,将它们推入队列。交换必须确切知道如何处理收到的消息。是否应将其附加到特定队列?是否应该将其附加到许多队列中?还是应该丢弃它。规则由交换类型定义 。

img

有几种交换类型可用:direct,topic,headers 和fanout。我们将集中讨论最后一个-扇出。让我们创建这种类型的交换,并将其称为log:

 channel.exchangeDeclare("logs", "fanout");

扇出交换非常简单。正如您可能从名称中猜测的那样,它只是将接收到的所有消息广播到它知道的所有队列中。而这正是我们记录器所需要的。

上市交易所

要列出服务器上的交换,您可以运行有用的rabbitmqctl:

 sudo rabbitmqctl list_exchanges

在此列表中,将有一些amq。*交换和默认(未命名)交换。这些是默认创建的,但是您现在不太可能需要使用它们。

无名交换

在本教程的前面部分中,我们对交换一无所知,但仍然能够将消息发送到队列。这是可能的,因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。

回想一下我们之前如何发布消息:

 channel.basicPublish(“”,“ hello”,null,message.getBytes());

第一个参数是交换的名称。空字符串表示默认或无名称交换:消息将以routingKey指定的名称路由到队列(如果存在)。

现在,我们可以改为发布到我们的命名交易所:

 channel.basicPublish( "logs", "", null, message.getBytes());

临时队列

您可能还记得,我们使用的是具有特定名称的队列(还记得hello和task_queue吗?)。能够命名队列对我们来说至关重要-我们需要将工人指向同一队列。当您想在生产者和消费者之间共享队列时,给队列命名很重要。

但这不是我们的记录器的情况。我们希望听到所有日志消息,而不仅仅是它们的一部分。我们也只对当前正在发送的消息感兴趣,而对旧消息不感兴趣。为了解决这个问题,我们需要两件事。

首先,无论何时连接到Rabbit,我们都需要一个全新的空队列。为此,我们可以创建一个具有随机名称的队列,或者甚至更好-让服务器为我们选择一个随机队列名称。

其次,一旦我们断开了使用者的连接,队列将被自动删除。

在Java客户端中,当我们不向queueDeclare()提供任何参数时,我们将 使用生成的名称创建一个非持久的,排他的,自动删除的队列:

 String queueName = channel.queueDeclare().getQueue();

您可以在队列指南中了解有关排他标志和其他队列属性的更多信息。

此时,queueName包含一个随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。

绑定

img

我们已经创建了一个扇出交换和一个队列。现在我们需要告诉交换机将消息发送到我们的队列。交换和队列之间的关系称为绑定

 channel.queueBind(queueName,“ logs”,“”);

从现在开始,logs交换将消息添加到我们的队列中。

列表绑定

您可以使用猜测的方式列出现有绑定,

 rabbitmqctl list_bindings

全部放在一起

img

产生日志消息的生产程序与上一教程看起来没有太大不同。最重要的变化是我们现在希望将消息发布到日志交换器,而不是无名的消息交换器。发送时,我们需要提供一个routingKey,但是对于扇出交换,它的值将被忽略。这是EmitLog.java程序的代码 :

 public class EmitLog {
 
   private static final String EXCHANGE_NAME = "logs";
 
   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.exchangeDeclare(EXCHANGE_NAME, "fanout");
 
         String message = argv.length < 1 ? "info: Hello World!" :
                             String.join(" ", argv);
 
         channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
         System.out.println(" [x] Sent '" + message + "'");
    }
  }
 }

如您所见,建立连接后,我们声明了交换。由于禁止发布到不存在的交易所,因此此步骤是必需的。

如果没有队列绑定到交换机,则消息将丢失,但这对我们来说是可以的。如果没有消费者在听,我们可以安全地丢弃该消息。

ReceiveLogs.java的代码:

 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.DeliverCallback;
 
 public class ReceiveLogs {
  private static final String EXCHANGE_NAME = "logs";
 
  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
 
    channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
    String queueName = channel.queueDeclare().getQueue();
    channel.queueBind(queueName, EXCHANGE_NAME, "");
 
    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 + "'");
    };
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
 }

结果的解释很简单:交换logs的数据进入具有服务器分配名称的两个队列。这正是我们的意图。

要了解如何侦听消息的子集,让我们继续学习 教程4

4. 路由(Routing)

有选择地接收消息

在上一教程中,我们构建了一个简单的日志记录系统。我们能够向许多接收者广播日志消息。

在本教程中,我们将向其中添加功能-我们将使仅订阅消息的子集成为可能。例如,我们将只能将严重错误消息定向到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

绑定

在前面的示例中,我们已经在创建绑定。您可能会想起以下代码:

 channel.queueBind(queueName, EXCHANGE_NAME, "");

绑定是交换和队列之间的关系。可以简单地理解为:队列对来自此交换的消息感兴趣。

绑定可以采用额外的routingKey参数。为了避免与basic_publish参数混淆,我们将其称为 绑定键。这是我们可以创建带有键的绑定的方法:

 channel.queueBind(queueName, EXCHANGE_NAME, "black");

绑定密钥的含义取决于交换类型。我们之前使用的 扇出交换只是忽略了它的价值。

直接交换

上一教程中的日志系统将所有消息广播给所有使用者。我们想要扩展它以允许根据邮件的严重性过滤邮件。例如,我们可能希望将日志消息写入磁盘的程序仅接收严重错误,而不会在警告或信息日志消息上浪费磁盘空间。

我们使用的是扇出交换,它并没有给我们带来太大的灵活性-它只能进行无意识的广播。

我们将使用直接交换。直接交换背后的路由算法很简单-消息进入其绑定密钥与消息的路由密钥完全匹配的队列 。

为了说明这一点,请考虑以下设置:

img

在此设置中,我们可以看到绑定了两个队列的直接交换X。第一个队列由绑定键orange绑定,第二个队列有两个绑定,一个绑定键为black,另一个绑定为green。

在这样的设置中,将使用路由键橙色将要发布到交换机的消息 路由到队列Q1。路由键为黑色 或绿色的消息将转到Q2。所有其他消息将被丢弃。

多重绑定

 

img

 

用相同的绑定密钥绑定多个队列是完全合法的。在我们的示例中,我们可以使用绑定键black在X和Q1之间添加绑定。在这种情况下,直接交换的行为将类似于扇出,并将消息广播到所有匹配的队列。带有黑色路由键的消息将同时传递给 Q1和Q2。

发射日志

我们将在记录系统中使用此模型。我们将发送消息到直接交换机,而不是扇出。我们将提供日志严重性作为路由键。这样,接收程序将能够选择它想要接收的严重性。让我们首先关注发射日志。

与往常一样,我们需要首先创建一个交换:

 channel.exchangeDeclare(EXCHANGE_NAME, "direct");

我们已经准备好发送一条消息:

 channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());

为简化起见,我们将假定“严重性”可以是“信息”,“警告”,“错误”之一。

订阅

接收消息的工作方式与上一教程一样,但有一个例外-我们将为感兴趣的每种严重性创建一个新的绑定。

 String queueName = channel.queueDeclare().getQueue();
 
 for(String severity : argv){
   channel.queueBind(queueName, EXCHANGE_NAME, severity);
 }

全部放在一起

img

EmitLogDirect.java类的代码:

 package cn.jjaxll.amqp.routing;
 
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 
 public class EmitLogDirect {
 
     private static final String EXCHANGE_NAME = "direct_logs";
 
     public static void main(String[] argv) throws Exception {
         ConnectionFactory factory = new ConnectionFactory();
         // 设置rabbit服务器地址
         factory.setHost("192.168.123.123");
         // 将创建连接放在try里面,try代码块结束之后会立刻释放资源
         try (Connection connection = factory.newConnection();
             // 创建通道
              Channel channel = connection.createChannel()) {
                 // 声明交换机             交换机名称   交换机模式/直连模式
                 channel.exchangeDeclare(EXCHANGE_NAME, "direct");
             for (int i = 0; i < 100; i++) {
                 // 消息
                 String message = getMessage(i);
                 // 路由key
                 String severity = getSeverity(i);
 
                 channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
                 System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
            }
 
        }
    }
 
     
     private static String getSeverity(int index) {
         if(index % 2 == 0){
             return "info";
        }else{
             return "error";
        }
    }
 
     private static String getMessage(int index) {
         if(index % 2 == 0){
             return "hello world";
        }else{
             return "error";
        }
    }
 
 }

ReceiveLogsDirect.java类的代码:

 package cn.jjaxll.amqp.routing;
 
 import com.rabbitmq.client.*;
 
 public class ReceiveLogsDirect {
 
  private static final String EXCHANGE_NAME = "direct_logs";
 
  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    // 设置rabbit服务器地址
    factory.setHost("192.168.123.123");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
     // 声明交换机           交换机名称         连接方式/直连模式
    channel.exchangeDeclare(EXCHANGE_NAME, "direct");
    // 队列名称
    String queueName = channel.queueDeclare().getQueue();
      System.out.println(String.format("queueName%s",queueName));
     // 绑定交换机到队列   队列名称     交换机名称       路由key
      channel.queueBind(queueName, EXCHANGE_NAME, "error");
 
    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 '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
    };
    // 自动确认消息
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
 }

ReceiveLogsDirect2.java类的代码:

 package cn.jjaxll.amqp.routing;
 
 import com.rabbitmq.client.*;
 
 public class ReceiveLogsDirect {
 
  private static final String EXCHANGE_NAME = "direct_logs";
 
  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    // 设置rabbit服务器地址
    factory.setHost("192.168.123.123");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
     // 声明交换机           交换机名称         连接方式/直连模式
    channel.exchangeDeclare(EXCHANGE_NAME, "direct");
    // 队列名称
    String queueName = channel.queueDeclare().getQueue();
      System.out.println(String.format("queueName%s",queueName));
     // 绑定交换机到队列   队列名称     交换机名称       路由key
      channel.queueBind(queueName, EXCHANGE_NAME, "info");
 
    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 '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
    };
    // 自动确认消息
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
 }

5. 话题(topics)

根据模式(主题)接收消息

在上一个教程中,我们改进了日志记录系统。代替使用仅能进行虚拟广播的扇出交换机,我们使用直接交换机,并有选择地接收日志的可能性。

尽管使用直接交换对我们的系统进行了改进,但它仍然存在局限性-它无法基于多个条件进行路由。

在我们的日志记录系统中,我们可能不仅要根据严重性订阅日志,还要根据发出日志的源订阅日志。您可能从syslog unix工具中了解了这个概念,该 工具根据严重性(info / warn / crit ...)和工具(auth / cron / kern ...)路由日志。

这将给我们带来很大的灵活性-我们可能只想听听来自'cron'的严重错误,也可以听听'kern'的所有日志。

为了在我们的日志系统中实现这一点,我们需要了解更复杂的主题交换。

话题交流

发送到主题交换机的消息不能具有任意的 routing_key-它必须是单词列表,以点分隔。这些词可以是任何东西,但通常它们指定与消息相关的某些功能。一些有效的路由关键示例:“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit ”。路由关键字中可以包含任意多个单词,最多255个字节。

绑定密钥也必须采用相同的形式。主题交换背后的逻辑 类似于直接交换-用特定路由键发送的消息将传递到所有用匹配绑定键绑定的队列。但是,绑定键有两个重要的特殊情况:

  • *(星号)可以代替一个单词。

  • #(哈希)可以替代零个或多个单词。

在一个示例中最容易解释这一点:

img

在此示例中,我们将发送所有描述动物的消息。将使用包含三个词(两个点)的路由密钥发送消息。路由键中的第一个单词将描述速度,第二个是颜色,第三个是物种:“ <speed><color><species> ”。

我们创建了三个绑定:Q1与绑定键“ * .orange。* ”绑定,Q2与“ 。rabbit ”和“ lazy。# ”绑定。

这些绑定可以总结为:

  • Q1对所有橙色动物都感兴趣。

  • 第2季想听听有关兔子的一切,以及有关懒惰动物的一切。

路由键设置为“ quick.orange.rabbit ”的消息将传递到两个队列。消息“ lazy.orange.elephant ”也将发送给他们两个。另一方面,“ quick.orange.fox ”只会进入第一个队列,而“ lazy.brown.fox ”只会进入第二个队列。“ lazy.pink.rabbit ”将被传递到第二队只有一次,即使两个绑定匹配。“ quick.brown.fox ”与任何绑定都不匹配,因此将被丢弃。

如果我们违反合同并发送一个或四个单词的消息,例如“橙色”或“ quick.orange.male.rabbit ”,会发生什么?好吧,这些消息将不匹配任何绑定,并且将会丢失。

另一方面,“ lazy.orange.male.rabbit ”即使有四个单词,也将匹配最后一个绑定,并将其传送到第二个队列。

话题交流

主题交流功能强大,可以像其他交流一样进行。

当队列用“ # ”(哈希)绑定键绑定时,它将接收所有消息,而与路由键无关,就像在扇出交换中一样。

当在绑定中不使用特殊字符“ * ”(星号)和“ # ”(哈希)时,主题交换的行为就像直接的一样。

全部放在一起

EmitLogTopic.java

 package cn.jjaxll.amqp.topic;
 
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 
 public class EmitLogTopic {
 
     private static final String EXCHANGE_NAME = "topic_logs";
 
     public static void main(String[] argv) throws Exception {
         ConnectionFactory factory = new ConnectionFactory();
         // 设置rabbit服务器地址
         factory.setHost("192.168.123.123");
         // 将创建连接放在try里面,try代码块结束之后会立刻释放资源
         try (Connection connection = factory.newConnection();
             // 创建通道
              Channel channel = connection.createChannel()) {
                 // 声明交换机             交换机名称   交换机模式/直连模式
                 channel.exchangeDeclare(EXCHANGE_NAME, "topic");
              int count = 1;
              for (int i = 0; i < 100; i++) {
                String message = "";
                String topic = "";
                if(count == 1){
                    message = "hello lazy";
                     topic = "lazy.#";
                     count++;
                }else if(count == 2){
                    message = "hello rabbit";
                    topic = "*.*.rabiit";
                    count++;
                }else if(count == 3){
                    message = "hello oragin";
                    topic = "*.oragin.*";
                    count++;
                }else if(count == 4) {
                    message = "违规的字符串";
                    topic = "asd.asd.adsa.asd.das";
                    count++;
                }else {
                    message = "lazy违规的字符串";
                    topic = "lazy.asd.adsa.asd.das";
                    count = 1;
                  }
 
                 channel.basicPublish(EXCHANGE_NAME, topic, null, message.getBytes("UTF-8"));
                 System.out.println(" [x] Sent '" + topic + "':'" + message + "'");
            }
 
        }
    }
 
     private static String getSeverity(int index) {
         if(index % 2 == 0){
             return "quite.oragin.rabbit";
        }else{
             return "error";
        }
    }
 
     private static String getMessage(int index) {
         if(index % 2 == 0){
             return "hello world";
        }else{
             return "error";
        }
    }
 
 }

ReceiveLogsTopic.java

 package cn.jjaxll.amqp.topic;
 
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.DeliverCallback;
 
 public class ReceiveLogsTopic {
 
  private static final String EXCHANGE_NAME = "topic_logs";
 
  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    // 设置rabbit服务器地址
    factory.setHost("192.168.123.123");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
     // 声明交换机           交换机名称         连接方式/直连模式
    channel.exchangeDeclare(EXCHANGE_NAME, "topic");
    // 队列名称
    String queueName = channel.queueDeclare().getQueue();
      System.out.println(String.format("queueName%s",queueName));
     // 绑定交换机到队列   队列名称     交换机名称       topic
      channel.queueBind(queueName, EXCHANGE_NAME, "*.oragin.*");
 
    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 '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
    };
    // 自动确认消息
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
 }

ReceiveLogsTopic2.java

 package cn.jjaxll.amqp.topic;
 
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.DeliverCallback;
 
 public class ReceiveLogsTopic2 {
 
  private static final String EXCHANGE_NAME = "topic_logs";
 
  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    // 设置rabbit服务器地址
    factory.setHost("192.168.123.123");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
     // 声明交换机           交换机名称         连接方式/直连模式
    channel.exchangeDeclare(EXCHANGE_NAME, "topic");
    // 队列名称
    String queueName = channel.queueDeclare().getQueue();
      System.out.println(String.format("queueName%s",queueName));
     // 绑定交换机到队列   队列名称     交换机名称       topic
      channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
      channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
 
    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 '" +
            delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
    };
    // 自动确认消息
    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
  }
 }

6. RPC

请求/回复模式示例

第二个教程中,我们学习了如何使用工作队列在多个工作人员之间分配耗时的任务。

但是,如果我们需要在远程计算机上运行功能并等待结果怎么办?好吧,那是一个不同的故事。这种模式通常称为“远程过程调用”或“ RPC”

在本教程中,我们将使用RabbitMQ构建RPC系统:客户端和可伸缩RPC服务器。由于我们没有值得分配的耗时任务,因此我们将创建一个虚拟RPC服务,该服务返回斐波那契数。

客户端界面

为了说明如何使用RPC服务,我们将创建一个简单的客户端类。它将公开一个名为call的方法,该方法 发送RPC请求并阻塞,直到收到答案为止

 FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
 String result = fibonacciRpc.call("4");
 System.out.println( "fib(4) is " + result);

有关RPC的说明

尽管RPC是计算中非常普遍的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是缓慢的RPC时,就会出现问题。这样的混乱会导致系统变幻莫测,并给调试增加了不必要的复杂性。滥用RPC可能会导致无法维护的意大利面条代码,而不是简化软件。

牢记这一点,请考虑以下建议:

  • 确保明显的是哪个函数调用是本地的,哪个是远程的。

  • 记录您的系统。明确组件之间的依赖关系。

  • 处理错误案例。RPC服务器长时间关闭后,客户端应如何反应?

如有疑问,请避免使用RPC。如果可以的话,应该使用异步管道-代替类似RPC的阻塞,将结果异步推送到下一个计算阶段。

回调队列

通常,通过RabbitMQ进行RPC很容易。客户端发送请求消息,服务器发送响应消息。为了接收响应,我们需要发送带有请求的“回调”队列地址。我们可以使用默认队列(在Java客户端中是唯一的)。让我们尝试一下:

 callbackQueueName = channel.queueDeclare().getQueue();
 
 BasicProperties props = new BasicProperties
                            .Builder()
                            .replyTo(callbackQueueName)
                            .build();
 
 channel.basicPublish("", "rpc_queue", props, message.getBytes());
 
 // ... then code to read a response message from the callback_queue ...
  • 讯息属性

    AMQP 0-9-1协议预定义了消息附带的14个属性集。除以下内容外,大多数属性很少使用:

    • deliveryMode:将消息标记为持久性(值为2)或瞬态(任何其他值)。您可能还记得第二个教程中的此属性。

    • contentType:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为application / json是一个好习惯。

    • replyTo:通常用于命名回调队列。

    • relatedId:用于将RPC响应与请求相关联。

我们需要这个新的导入:

 import com.rabbitmq.client.AMQP.BasicProperties;

关联ID

在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。那是相当低效的,但是幸运的是有更好的方法-让我们为每个客户端创建一个回调队列。

这就引发了一个新问题,在该队列中收到响应后,尚不清楚响应属于哪个请求。那就是当使用correlationId属性时 。我们将为每个请求将其设置为唯一值。稍后,当我们在回调队列中收到消息时,我们将查看该属性,并基于此属性将响应与请求进行匹配。如果我们看到一个未知的 correlationId值,我们可以安全地丢弃该消息-它不属于我们的请求。

您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是因错误而失败?这是由于服务器端可能出现竞争状况。尽管可能性不大,但RPC服务器可能会在向我们发送答案之后但在发送请求的确认消息之前死亡。如果发生这种情况,重新启动的RPC服务器将再次处理该请求。这就是为什么在客户端上我们必须妥善处理重复的响应,并且理想情况下RPC应该是幂等的。

概要

img

我们的RPC将像这样工作:

  • 对于RPC请求,客户端发送一条具有两个属性的消息: replyTo(设置为仅为该请求创建的匿名互斥队列)和correlationId(设置为每个请求的唯一值)。

  • 该请求被发送到rpc_queue队列。

  • RPC工作程序(又名:服务器)正在等待该队列上的请求。出现请求时,它会使用replyTo字段中的队列来完成工作并将带有结果的消息发送回客户端。

  • 客户端等待答复队列中的数据。出现消息时,它会检查correlationId属性。如果它与请求中的值匹配,则将响应返回给应用程序。

全部放在一起

斐波那契任务:

 private static int fib(int n) {
     if (n == 0) return 0;
     if (n == 1) return 1;
     return fib(n-1) + fib(n-2);
 }

我们声明我们的斐波那契函数。它仅假定有效的正整数输入。(不要指望这种方法适用于大量用户,它可能是最慢的递归实现)。

RPCClient.java

 package cn.jjaxll.amqp.rpc;
 
 import com.rabbitmq.client.AMQP;
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeoutException;
 
 public class RPCClient implements AutoCloseable {
 
     private Connection connection;
     private Channel channel;
     private String requestQueueName = "rpc_queue";
 
     public RPCClient() throws IOException, TimeoutException {
         ConnectionFactory factory = new ConnectionFactory();
         factory.setHost("192.168.123.123");
 
         connection = factory.newConnection();
         channel = connection.createChannel();
    }
 
     public static void main(String[] argv) {
         // 创建RPC客户端
         try (RPCClient fibonacciRpc = new RPCClient()) {
             // 循环发送32次
             for (int i = 0; i < 32; i++) {
                 String i_str = Integer.toString(i);
                 System.out.println(" [x] Requesting fib(" + i_str + ")");
                 String response = fibonacciRpc.call(i_str);
                 System.out.println(" [.] Got '" + response + "'");
            }
        } catch (IOException | TimeoutException | InterruptedException e) {
             e.printStackTrace();
        }
    }
 
     public String call(String message) throws IOException, InterruptedException {
         // 请求ID
         final String corrId = UUID.randomUUID().toString();
         // 回调队列名称
         String replyQueueName = channel.queueDeclare().getQueue();
         // 构造基本属性
         AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();
 
         channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
 
         // 响应队列 长度为1的堵塞队列
         final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
 
         // 监听消息
         String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
             // 如果是自己的消息,就将消息体传入到响应队列中
             if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                 response.offer(new String(delivery.getBody(), StandardCharsets.UTF_8));
            }
        }, System.out::println);
 
         // 取出
         String result = response.take();
         channel.basicCancel(ctag);
         return result;
    }
 
     public void close() throws IOException {
         connection.close();
    }
 }

RPCServer.java

 package cn.jjaxll.amqp.rpc;
 
 import com.rabbitmq.client.*;
 
 public class RPCServer {
 
     private static final String RPC_QUEUE_NAME = "rpc_queue";
 
     //
     private static int fib(int n) {
         if (n == 0) return 0;
         if (n == 1) return 1;
         return fib(n - 1) + fib(n - 2);
    }
 
     public static void main(String[] argv) throws Exception {
         ConnectionFactory factory = new ConnectionFactory();
         factory.setHost("192.168.123.123");
         // 创建连接
         try (Connection connection = factory.newConnection();
              Channel channel = connection.createChannel()) {
             // 声明队列
             channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
             // 清理队列
             channel.queuePurge(RPC_QUEUE_NAME);
             // 设置最多只能接收一个没有确认的消息
             channel.basicQos(1);
 
             System.out.println(" [x] Awaiting RPC requests");
 
             Object monitor = new Object();
             // 回调函数
             DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                 AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();
 
                 String response = "";
 
                 try {
                     String message = new String(delivery.getBody(), "UTF-8");
                     int n = Integer.parseInt(message);
 
                     System.out.println(" [.] fib(" + message + ")");
                     response += fib(n);
                } catch (RuntimeException e) {
                     System.out.println(" [.] " + e.toString());
                } finally {
                     channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
                     channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                     try {
                         Thread.sleep(1000);
                    } catch (InterruptedException e) {
                         e.printStackTrace();
                    }
                     // RabbitMq consumer worker thread notifies the RPC server owner thread
                     synchronized (monitor) {
                         monitor.notify();
                    }
                }
            };
 
             channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
             // Wait and be prepared to consume the message from RPC client.
             while (true) {
                 synchronized (monitor) {
                     try {
                         monitor.wait();
                    } catch (InterruptedException e) {
                         e.printStackTrace();
                    }
                }
            }
        }
    }
 }

服务器代码非常简单:

  • 与往常一样,我们首先建立连接,通道并声明队列。

  • 我们可能要运行多个服务器进程。为了将负载平均分配到多个服务器上,我们需要在channel.basicQos中设置 prefetchCount设置。

  • 我们使用basicConsume访问队列,在队列中我们以对象(DeliverCallback)的形式提供回调,该回调将完成工作并将响应发送回去。

可以在这里找到我们的RPC客户端的代码:RPCClient.java

客户端代码稍微复杂一些:

  • 我们建立连接和渠道。

  • 我们的调用方法发出实际的RPC请求。

  • 在这里,我们首先生成一个唯一的relatedId 编号并将其保存-我们的使用者回调将使用该值来匹配适当的响应。

  • 然后,我们为回复创建一个专用的排他队列并订阅。

  • 接下来,我们发布具有两个属性的请求消息: replyTo和correlationId。

  • 在这一点上,我们可以坐下来等到正确的响应到达。

  • 由于我们的消费者交付处理是在单独的线程中进行的,因此在响应到达之前,我们将需要一些东西来挂起主线程。使用BlockingQueue是一种可行的解决方案。在这里,我们正在创建 容量设置为1的ArrayBlockingQueue,因为我们只需要等待一个响应即可。

  • 消费者的工作非常简单,对于每一个消耗的响应消息,它都会检查correlationId 是否为我们要寻找的消息。如果是这样,它将响应放入BlockingQueue。

  • 同时,主线程正在等待响应,以将其从BlockingQueue中获取。

  • 最后,我们将响应返回给用户。

发出客户请求:

 RPCClient fibonacciRpc = new RPCClient();
 
 System.out.println(" [x] Requesting fib(30)");
 String response = fibonacciRpc.call("30");
 System.out.println(" [.] Got '" + response + "'");
 
 fibonacciRpc.close();

这里介绍的设计不是RPC服务的唯一可能的实现,但是它具有一些重要的优点:

  • 如果RPC服务器太慢,则可以通过运行另一台RPC服务器来进行扩展。尝试在新控制台中运行第二个RPCServer。

  • 在客户端,RPC只需要发送和接收一条消息。不需要诸如queueDeclare之 类的同步调用。结果,RPC客户端只需要一个网络往返就可以处理单个RPC请求。

我们的代码仍然非常简单,并且不会尝试解决更复杂(但很重要)的问题,例如:

  • 如果没有服务器在运行,客户端应如何反应?

  • 客户端是否应该为RPC设置某种超时时间?

  • 如果服务器发生故障并引发异常,是否应该将其转发给客户端?

  • 在处理之前防止无效的传入消息(例如检查边界,类型)。

发布者确认(Publisher Confirms)

发布者确认 是RabbitMQ扩展,可以实现可靠的发布。在通道上启用发布者确认后,代理将异步确认客户端发布的消息,这意味着它们已在服务器端处理。

在本教程中,我们将使用发布者确认来确保发布的消息已安全到达代理。我们将介绍几种使用发布商确认并解释其优缺点的策略。

在频道上启用发布者确认

发布者确认是AMQP 0.9.1协议的RabbitMQ扩展,因此默认情况下未启用它们。发布者确认是通过ConfirmSelect方法在渠道级别启用的:

 Channel channel = connection.createChannel();
 channel.confirmSelect();

必须在希望使用发布者确认的每个频道上调用此方法。确认仅应启用一次,而不是对每个已发布的消息都启用。

策略1:分别发布消息

让我们从使用确认发布的最简单方法开始,即发布消息并同步等待其确认:

 while (thereAreMessagesToPublish()) {
     byte[] body = ...;
     BasicProperties properties = ...;
     channel.basicPublish(exchange, queue, properties, body);
     // uses a 5 second timeout
     channel.waitForConfirmsOrDie(5_000);
 }

在前面的示例中,我们像往常一样发布一条消息,并等待通过Channel#waitForConfirmsOrDie(long)方法对其进行确认。确认消息后,该方法立即返回。如果未在超时时间内确认该消息或该消息没有被确认(这意味着代理出于某种原因无法处理该消息),则该方法将引发异常。异常的处理通常包括记录错误消息和/或重试发送消息。

不同的客户端库有不同的方式来同步处理发布者的确认,因此请确保仔细阅读所使用客户端的文档。

此技术非常简单,但也有一个主要缺点:由于消息的确认会阻止所有后续消息的发布,因此它会大大降低发布速度。这种方法不会提供每秒超过数百条已发布消息的吞吐量。但是,对于某些应用程序来说这可能已经足够了。

发布者确认异步吗?

我们在一开始提到代理程序以异步方式确认发布的消息,但是在第一个示例中,代码同步等待直到消息被确认。客户端实际上异步接收确认,并相应地取消阻止对waitForConfirmsOrDie的调用 。可以将waitForConfirmsOrDie视为依赖于幕后异步通知的同步帮助器。

策略2:批量发布消息

为了改进前面的示例,我们可以发布一批消息,并等待整个批次被确认。以下示例使用了100个批次:

 int batchSize = 100;
 int outstandingMessageCount = 0;
 while (thereAreMessagesToPublish()) {
     byte[] body = ...;
     BasicProperties properties = ...;
     channel.basicPublish(exchange, queue, properties, body);
     outstandingMessageCount++;
     if (outstandingMessageCount == batchSize) {
         ch.waitForConfirmsOrDie(5_000);
         outstandingMessageCount = 0;
    }
 }
 if (outstandingMessageCount > 0) {
     ch.waitForConfirmsOrDie(5_000);
 }

与等待确认单个消息相比,等待一批消息被确认可以极大地提高吞吐量(对于远程RabbitMQ节点,这最多可以达到20-30次)。缺点之一是我们不知道发生故障时到底出了什么问题,因此我们可能必须将整个批处理保存在内存中,以记录有意义的内容或重新发布消息。而且该解决方案仍然是同步的,因此它阻止了消息的发布。

策略3:处理发布者异步确认

代理异步确认已发布的消息,只需在客户端上注册一个回调即可收到这些确认的通知:

 Channel channel = connection.createChannel();
 channel.confirmSelect();
 channel.addConfirmListener((sequenceNumber, multiple) -> {
     // code when message is confirmed
 }, (sequenceNumber, multiple) -> {
     // code when message is nack-ed
 });

有2个回调:一个用于确认的消息,一个用于未确认的消息(代理可以认为丢失的消息)。每个回调都有2个参数:

  • 序列号:标识已确认或未确认消息的数字。我们很快将看到如何将其与已发布的消息相关联。

  • 整数:这是一个布尔值。如果为false,则仅确认/否定一条消息;如果为true,则将确认/无序列号较低或相等的所有消息。

可以 在发布之前使用Channel#getNextPublishSeqNo()获得序列号:

 int sequenceNumber = channel.getNextPublishSeqNo());
 ch.basicPublish(exchange, queue, properties, body);

将消息与序列号关联的一种简单方法是使用映射。假设我们要发布字符串,因为它们很容易变成要发布的字节数组。这是一个使用映射将发布序列号与消息的字符串主体相关联的代码示例:

 ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
 // ... code for confirm callbacks will come later
 String body = "...";
 outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
 channel.basicPublish(exchange, queue, properties, body.getBytes());

现在,发布代码使用地图跟踪出站邮件。我们需要在确认到达时清理此地图,并做一些类似在消息不足时记录警告的操作:

 ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
 ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
     if (multiple) {
         ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
           sequenceNumber, true
        );
         confirmed.clear();
    } else {
         outstandingConfirms.remove(sequenceNumber);
    }
 };
 
 channel.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
     String body = outstandingConfirms.get(sequenceNumber);
     System.err.format(
       "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
       body, sequenceNumber, multiple
    );
     cleanOutstandingConfirms.handle(sequenceNumber, multiple);
 });
 // ... publishing code

上一个示例包含一个回调,当确认到达时,该回调将清除地图。请注意,此回调处理单个确认和多个确认。确认到达时使用此回调(作为Channel#addConfirmListener的第一个参数 )。缺少邮件的回调将检索邮件正文并发出警告。然后,它重新使用前一个回调来清理未完成确认的映射(无论消息是已确认还是未确认,都必须删除它们在映射中的对应条目。)

如何跟踪未完成的确认?

我们的示例使用ConcurrentNavigableMap来跟踪未完成的确认。由于以下几个原因,此数据结构很方便。它允许轻松地将序列号与消息相关联(无论消息数据是什么),还可以轻松清除条目直到给定的序列ID(以处理多个确认/提示)。最后,它支持并发访问,因为在客户端库拥有的线程中调用了确认回调,该线程应与发布线程保持不同。

除了使用复杂的映射实现之外,还有其他跟踪未完成确认的方法,例如使用简单的并发哈希映射和变量来跟踪发布序列的下限,但是它们通常涉及更多且不属于教程。

综上所述,处理发布者异步确认通常需要执行以下步骤:

  • 提供一种将发布序列号与消息相关联的方法。

  • 在通道上注册一个确认侦听器,以便在发布者确认/采取措施执行适当的操作(例如记录或重新发布未确认的消息)时收到通知。序列号与消息的关联机制在此步骤中可能还需要进行一些清洗。

  • 在发布消息之前跟踪发布序列号。

重新发布nack-ed消息?

从相应的回调中重新发布一个nack-ed消息可能很诱人,但是应该避免这种情况,因为确认回调是在不应执行通道的I / O线程中分派的。更好的解决方案是将消息放入由发布线程轮询的内存队列中。诸如ConcurrentLinkedQueue之类的类 将是在确认回调和发布线程之间传输消息的理想选择。

概要

在某些应用程序中,确保将发布的消息发送到代理非常重要。发布者确认是RabbitMQ功能,可以帮助满足此要求。发布者确认本质上是异步的,但也可以同步处理它们。没有确定的方法可以实现发布者确认,这通常归结为应用程序和整个系统中的约束。典型的技术有:

  • 单独发布消息,同步等待确认:简单,但吞吐量非常有限。

  • 批量发布消息,同步等待批量确认:简单,合理的吞吐量,但是很难推断出什么时候出了问题。

  • 异步处理:最佳性能和资源使用,在出现错误的情况下可以很好地控制,但可以正确实施。

放在一起

PublisherConfirms.java 类包含了我们所覆盖的技术代码。我们可以对其进行编译,按原样执行并查看它们各自的性能:

 package cn.jjaxll.amqp;
 
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.ConfirmCallback;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
 
 import java.time.Duration;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentNavigableMap;
 import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.function.BooleanSupplier;
 
 public class PublisherConfirms {
 
     static final int MESSAGE_COUNT = 50_000;
 
     static Connection createConnection() throws Exception {
         ConnectionFactory cf = new ConnectionFactory();
         cf.setHost("192.168.123.123");
         cf.setUsername("guest");
         cf.setPassword("guest");
         return cf.newConnection();
    }
 
     public static void main(String[] args) throws Exception {
         publishMessagesIndividually();
         publishMessagesInBatch();
         handlePublishConfirmsAsynchronously();
    }
 
     static void publishMessagesIndividually() throws Exception {
         try (Connection connection = createConnection()) {
             Channel ch = connection.createChannel();
 
             String queue = UUID.randomUUID().toString();
             ch.queueDeclare(queue, false, false, true, null);
 
             ch.confirmSelect();
             long start = System.nanoTime();
             for (int i = 0; i < MESSAGE_COUNT; i++) {
                 String body = String.valueOf(i);
                 ch.basicPublish("", queue, null, body.getBytes());
                 ch.waitForConfirmsOrDie(5_000);
            }
             long end = System.nanoTime();
             System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
        }
    }
 
     static void publishMessagesInBatch() throws Exception {
         try (Connection connection = createConnection()) {
             Channel ch = connection.createChannel();
 
             String queue = UUID.randomUUID().toString();
             ch.queueDeclare(queue, false, false, true, null);
 
             ch.confirmSelect();
 
             int batchSize = 100;
             int outstandingMessageCount = 0;
 
             long start = System.nanoTime();
             for (int i = 0; i < MESSAGE_COUNT; i++) {
                 String body = String.valueOf(i);
                 ch.basicPublish("", queue, null, body.getBytes());
                 outstandingMessageCount++;
 
                 if (outstandingMessageCount == batchSize) {
                     ch.waitForConfirmsOrDie(5_000);
                     outstandingMessageCount = 0;
                }
            }
 
             if (outstandingMessageCount > 0) {
                 ch.waitForConfirmsOrDie(5_000);
            }
             long end = System.nanoTime();
             System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
        }
    }
 
     static void handlePublishConfirmsAsynchronously() throws Exception {
         try (Connection connection = createConnection()) {
             Channel ch = connection.createChannel();
 
             String queue = UUID.randomUUID().toString();
             ch.queueDeclare(queue, false, false, true, null);
 
             ch.confirmSelect();
 
             ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
 
             ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
                 if (multiple) {
                     ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
                             sequenceNumber, true
                    );
                     confirmed.clear();
                } else {
                     outstandingConfirms.remove(sequenceNumber);
                }
            };
 
             ch.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
                 String body = outstandingConfirms.get(sequenceNumber);
                 System.err.format(
                         "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
                         body, sequenceNumber, multiple
                );
                 cleanOutstandingConfirms.handle(sequenceNumber, multiple);
            });
 
             long start = System.nanoTime();
             for (int i = 0; i < MESSAGE_COUNT; i++) {
                 String body = String.valueOf(i); 
               outstandingConfirms.put(ch.getNextPublishSeqNo(), body); 
               ch.basicPublish("", queue, null, body.getBytes()); 
          } 
​ 
           if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) { 
               throw new IllegalStateException("All messages could not be confirmed in 60 seconds"); 
          } 
​ 
           long end = System.nanoTime(); 
           System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n", MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis()); 
      } 
  } 
​ 
   static boolean waitUntil(Duration timeout, BooleanSupplier condition) throws InterruptedException { 
       int waited = 0; 
       while (!condition.getAsBoolean() && waited < timeout.toMillis()) { 
           Thread.sleep(100L); 
           waited = +100; 
      } 
       return condition.getAsBoolean(); 
  } 
​ 
}

输出将如下所示:

 在5,549毫秒内单独发布50,000条消息
 在2,331毫秒内批量发布50,000条消息
 在4,054毫秒内发布50,000条消息并进行异步确认

我们看到现在单独发布的效果非常好。但是,通过客户端和服务器之间的网络,批处理发布和异步处理现在的执行方式类似,对于发布者确认的异步处理来说,这是一个很小的优势。

请记住,批量发布很容易实现,但是在发布者否定确认的情况下,不容易知道哪些消息无法发送给代理。处理发布者确认异步涉及更多的实现,但是提供更好的粒度和更好地控制在处理发布的消息时执行的动作。

 

posted @ 2020-12-14 16:00  jainz  阅读(256)  评论(0)    收藏  举报