rabbitMq

AMQP

          AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个提供统一消息服务的应用层标准高级消息队列是什协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。

(应用层协议,开放标准,与实现无关)

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

RabbitMQ 是一个可靠且成熟的消息传递和流代理,易于部署在云环境、本地和本地计算机上。它目前被全球数百万人使用。

AMQP定义网络协议和代理服务如下

一套确定的消息交换功能,也就是“高级消息交换协议模型” 路由、存储、消息交换

AMQP模型

术语

连接(Connection):一个网络连接,比如TCP/IP套接字连接。

会话(Session):端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。

信道(Channel):多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。

客户端(Client):AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。

服务器(Server):接受客户端连接,实现AMQP消息队列和路由功能的进程。也称为“消息代理”。

交换器(Exchange):服务器中的实体,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

交换器类型(Exchange Type):基于不同路由语义的交换器类。

消息队列(Message Queue):一个命名实体,用来保存消息直到发送给消费者。

绑定器(Binding):消息队列和交换器之间的关联。

绑定器关键字(Binding Key):绑定的名称。一些交换器类型可能使用这个名称作为定义绑定器路由行为的模式。

路由关键字(Routing Key):一个消息头,交换器可以用这个消息头决定如何路由某条消息。

持久存储(Durable):一种服务器资源,当服务器重启时,保存的消息数据不会丢失。

临时存储(Transient):一种服务器资源,当服务器重启时,保存的消息数据会丢失。

持久化(Persistent):服务器将消息保存在可靠磁盘存储中,当服务器重启时,消息不会丢失。

非持久化(Non-Persistent):服务器将消息保存在内存中,当服务器重启时,消息可能丢失。

虚拟主机(Virtual Host):一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。客户端应用程序在登录到服务器之后,可以选择一个虚拟主机。

主题:通常指发布消息;AMQP规范用一种或多种交换器来实现主题。

许可证

自 2007 年首次发布以来,RabbitMQ 是免费和开源软件。此外,Broadcom 还提供一系列商业产品。

RabbitMQ 在 Apache 许可证 2.0 和 Mozilla 公共许可证 2 下获得双重许可。您可以随心所欲地使用和修改 RabbitMQ。

github

RabbitMQ · GitHub

rabbitmq-server 

 rabbitmq-java-client 

rabbitmq-website 

安装

最新版本是 RabbitMQ 的 3.13.3

 docker 镜像:hub.docker.com

# latest RabbitMQ 3.13
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.13-management

RabbitMQ 服务器

 Erlang 版本要求:  Erlang 和         .26.x   25.x

Installing on Windows | RabbitMQ

rabbitmq-server-generic-unix-3.13.3.tar.xzicon-default.png?t=N7T8https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.13.3/rabbitmq-server-generic-unix-3.13.3.tar.xz

CLI 工具

RabbitMQ 节点通常使用 PowerShell 中的 CLI 工具进行管理、检查和操作。

在 Windows 上,与其他平台相比,CLI 工具具有后缀。例如,在 Windows 上被调用为 ..batrabbitmqctlrabbitmqctl.bat

要了解各种 RabbitMQ CLI 工具提供的命令,请使用以下命令:help

# lists commands provided by rabbitmqctl.bat
rabbitmqctl.bat help

# lists commands provided by rabbitmq-diagnostics.bat
rabbitmq-diagnostics.bat help

# ...you guessed it!
rabbitmq-plugins.bat help

管理 RabbitMQ 节点

管理服务

可以在“开始”菜单中找到指向 RabbitMQ 目录的链接。

还有一个指向命令提示符窗口的链接,该窗口 将在 sbin 目录的“开始”菜单中启动。这是 运行命令行工具的最便捷方式。

请注意,CLI 工具必须对目标 RabbitMQ 节点进行身份验证

停止节点

要停止代理或检查其状态,请使用 in(以管理员身份)。rabbitmqctl.batsbin

rabbitmqctl.bat stop

检查节点状态

以下 CLI 命令运行基本运行状况检查,并显示有关节点的一些信息(如果节点正在运行)。

# A basic health check of both the node and CLI tool connectivity/authentication
rabbitmqctl.bat status

为了让它工作, 必须满足两个条件:

  • 节点必须正在运行
  • rabbitmqctl.bat必须能够向节点进行身份验证

1、发布者、交换机、队列、消费者都可以有多个。同时因为 AMQP 是一个网络协议,所以这个过程中的发布者,消费者,消息代理 可以分别存在于不同的设备上。

2、发布者发布消息时可以给消息指定各种消息属性(Message Meta-data)。有些属性有可能会被消息代理(Brokers)使用,然而其他的属性则是完全不透明的,它们只能被接收消息的应用所使用。

3、从安全角度考虑,网络是不可靠的,又或是消费者在处理消息的过程中意外挂掉,这样没有处理成功的消息就会丢失。基于此原因,AMQP 模块包含了一个消息确认(Message Acknowledgements)机制:当一个消息从队列中投递给消费者后,不会立即从队列中删除,直到它收到来自消费者的确认回执(Acknowledgement)后,才完全从队列中删除。

4、在某些情况下,例如当一个消息无法被成功路由时(无法从交换机分发到队列),消息或许会被返回给发布者并被丢弃。或者,如果消息代理执行了延期操作,消息会被放入一个所谓的死信队列中。此时,消息发布者可以选择某些参数来处理这些特殊情况。

Exchange交换机

交换机是用来发送消息的 AMQP 实体。

交换机拿到一个消息之后将它路由给一个或零个队列。

它使用哪种路由算法是由交换机类型和绑定(Bindings)规则所决定的。

交换机类型

Direct Exchange(直连交换机) (Empty String) and amq.direct

Fanout Exchange(扇形交换机) amq.fanout

Topic Exchange(主题交换机) amq.topic

Headers Exchange(头交换机) amq.match(and amq.headers in rabbitMQ)

除交换机类型外,在声明交换机时还可以附带其它属性,分别是:

Name

Durability (消息代理重启后,交换机是否还存在)

Auto-delete (当所有与之绑定的队列都完成了对此交换机的使用后,删除它)

Aruguments (依赖代理本身)

交换机状态

持久(durable)、暂存(transient)。

durable交换机消息代理重启后依旧存在

暂存的交换机则不会(它们需要在代理再次上线后重新被声明)

并不是所有的应用场景都需要持久化的交换机。

默认交换机

默认交换机(default exchange)实际上是一个由消息代理预先声明好的没有名字(名字为空字符串)的直连交换机(direct exchange)。

它有一个特殊的属性使得它对于简单应用特别有用处:那就是每个新建队列(queue)都会自动绑定到默认交换机上,绑定的路由键(routing key)名称与队列名称相同。

队列教程(Queue tutorials)

1. "Hello World!"  

编写程序以发送和接收来自命名队列的消息

Sending

We'll call our message publisher (sender) Send and our message consumer (receiver) Recv. The publisher will connect to RabbitMQ, send a single message, then exit.

rabbitmq-tutorials/java/Send.java at main · rabbitmq/rabbitmq-tutorials · GitHub

Send.java

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 {
      ...
  }
}

//创建与服务器的连接:
x
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
     Channel channel = connection.createChannel()) {

}

//要发送,我们必须声明一个队列供我们发送到;然后我们可以发布一条消息 到队列中,所有这些都在 try-with-resources 语句中

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

/**全部send代码**/

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

import java.nio.charset.StandardCharsets;

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(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

接收

们的消费者会收听来自 RabbitMQ,所以与发布单个消息的发布者不同,我们将 让使用者保持运行以侦听消息并将其打印出来。

Recv.java

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



//设置与发布者相同;我们打开一个连接和一个 channel,并声明我们将要从中使用的队列。 请注意,这与发布到的队列匹配。
//注意,我们也在此处声明队列。因为我们可能会开始 消费者先于发布者,我们要确保队列存在 在我们尝试从中消费消息之前。
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 deliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });



//完整Recv.java

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.nio.charset.StandardCharsets;

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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}





2. Work Queues(任务队列

创建一个工作队列,用于分发 多个工作人员之间的耗时任务。

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

示例:

NewTask.java

Worker.java

NewTask.java 将任务安排到我们的工作队列中

String message = String.join(" ", argv);
channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");


Worker.java 它将处理 传递消息并执行任务

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

模拟执行时间的假任务:

private static void doWork(String task) throws InterruptedException {
    for (char ch: task.toCharArray()) {
        if (ch == '.') Thread.sleep(1000);
    }
}

消息确认 autoAck=true false

// accept only one unack-ed message at a time (see below)
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 -> { });

消息持久性(已经声明相同名称的队列,此定义不生效)

boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);
//定义另外一个队列 此更改需要应用于生产者 和消费者代码。queueDeclare
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
//我们需要将消息标记为持久性task_queue
import com.rabbitmq.client.MessageProperties;
channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

Fair dispatch(公平调度)

在有两个工人的情况下,当所有 奇数消息很重,偶数消息很轻,一个工人会 一直很忙,另一个人几乎不做任何工作。井 RabbitMQ 对此一无所知,仍然会调度 消息均匀。

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

为了解决这个问题,we can use the basicQos method with the prefetchCount = 1 setting.。这告诉 RabbitMQ 不要给超过 一次向工作人员发送一条消息。或者,换句话说,不要派遣 向工作人员发送一条新消息,直到它处理并确认 上一个。相反,它会将其分派给下一个尚未忙碌的工作人员。basicQosprefetchCount1

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

关于队列大小的注意事项

If all the workers are busy, your queue can fill up. You will want to keep an eye on that, and maybe add more workers, or have some other strategy.

以上内容集成在一起 完整代码 NewTask.java  Worker.java

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

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

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

它将由两个程序组成 - 第一个程序将发出日志 消息,第二个将接收并打印它们。

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

从本质上讲,已发布的日志消息将广播给所有人 接收器。

示例:构建一个简单的日志记录 系统

回顾一下我们在前面的教程中介绍的内容:

  • 生产者是发送消息的用户应用程序。
  • 队列是存储消息的缓冲区。
  • 使用者是接收消息的用户应用程序。

RabbitMQ 中消息传递模型的核心思想是生产者 从不直接向队列发送任何消息。实际上,很多时候 生产者甚至不知道消息是否会传递给任何 完全排队。

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

There are a few exchange types available: directtopicheaders and fanout. We'll focus on the last one -- the fanout. Let's create an exchange of this type, and call it logs:

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

The fanout exchange is very simple. (fanout exchange)

Listing exchanges
sudo rabbitmqctl list_exchanges

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

In this list there will be some amq.* exchanges and the default (unnamed) exchange. These are created by default, but it is unlikely you'll need to use them at the moment.

Nameless exchange

在前几部分中,我们对交换一无所知, 但仍然能够将消息发送到队列。这是可能的 因为我们使用的是默认交换,我们用空字符串 () 来标识它。""

In previous parts of the tutorial we knew nothing about exchanges, but still were able to send messages to queues. That was possible because we were using a default exchange, which we identify by the empty string ("").

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

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

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

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

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

Temporary queues(临时队列)

以前我们使用的队列具有 特定名称(还记得和?能够命名 排队对我们来说至关重要——我们需要将工人指向 相同的队列。当您 希望在生产者和消费者之间共享队列。hello task_queue

但对于我们的记录器来说,情况并非如此。我们想听听所有 日志消息,而不仅仅是其中的子集。我们是 也只对当前流动的消息感兴趣,而不是对旧的消息感兴趣 的。为了解决这个问题,我们需要两件事。

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

其次,一旦我们断开消费者的连接,队列应该是 自动删除。

我们不提供任何参数时,我们会创建一个非持久的、独占的、具有生成名称的自动删除队列:queueDeclare()

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

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

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

We've already created a fanout exchange and a queue. Now we need to tell the exchange to send messages to our queue. That relationship between exchange and a queue is called a binding.

已经创建一个 fanout exchange 和一个 queue. 现在需要告诉exchange 为了发送消息到队列,

这种exchange 和 队列的关系叫做绑定

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

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

列表绑定 Listing bindings(展示绑定关系)

可以使用以下方法列出现有绑定,

rabbitmqctl list_bindings

发出日志消息的 producer 程序看起来并不多 

我们现在希望将消息发布到我们的exchange,而不是 无名的。发送时我们需要提供一个,但它 交换的值被忽略。这是程序的代码:logs routingKey fanout 

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

验证代码是否实际 根据需要创建绑定和队列。运行两个程序后,您应该会看到如下内容:rabbitmqctl

sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs    exchange        amq.gen-JzTY20BRgKO-HjmUJj0wLg  queue           []
# => logs    exchange        amq.gen-vso0PVvyiRIL2WoV3i48Yg  queue           []
# => ...done.

4. Routing (路由)

创建了绑定

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

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

绑定可以采用额外的参数。为了避免 与参数混淆,我们将其称为 .这就是我们如何使用键创建绑定的方法:routingKey basic_publish binding key

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

binding key含义取决于交换类型。我们之前使用的交易所只是忽略了它的 价值。fanout

Direct exchange

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

我们使用的是fanout exchange,这并没有给我们太多 灵活性 - 它只能进行无意识的广播。

我们将改用direct  exchange。背后的路由算法 direct exchange很简单 - 消息进入与消息完全匹配的队列。

We will use a direct exchange instead. The routing algorithm behind a direct exchange is simple - a message goes to the queues whose binding key exactly matches the routing key of the message.

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

在此设置中,我们可以看到绑定了两个队列的交换 到它。第一个队列绑定了绑定键,第二个队列绑定了绑定键 有两个绑定,一个带有绑定键,另一个带有绑定键 跟。directXorangeblackgreen

In this setup, we can see the direct exchange X with two queues bound to it. The first queue is bound with binding key orange, and the second has two bindings, one with binding key black and the other one with green.

在这样的设置中,使用routing key orange发布到exchange的邮件将被路由到队列Q1。routing key 为 black或green 将会到Q2。所有其他消息将被丢弃。

In such a setup a message published to the exchange with a routing key orange will be routed to queue Q1. Messages with a routing key of black or green will go to Q2. All other messages will be discarded.

Multiple bindings (多绑定)

It is perfectly legal to bind multiple queues with the same binding key. In our example we could add a binding between X and Q1 with binding key black. In that case, the direct exchange will behave like fanout and will broadcast the message to all the matching queues. A message with routing key black will be delivered to both Q1 and Q2.

合法的用相同的routing key 绑定多个队列,在我们的例子中,我们能够添加一个黑色的bingding key  从X到Q1队列,在这个例子中,direct exchange 的行为像fanout 广播message 到所有的匹配队列,一个消息,将被传递到Q1 和 Q2

发出日志 (Emitting logs)

我们将此模型用于日志记录系统。取代fanout exchange。我们将发送一个direct exchange.

我们申请这个log作为一个routing key ,这样接收程序将可能选择想接收的。

As always, we need to create an exchange first:

channel.exchangeDeclare(EXCHANGE_NAME, "direct");

And we're ready to send a message:

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

To simplify things we will assume that 'severity' can be one of 'info', 'warning', 'error'.

订阅 Subscribing

接收消息的工作方式与上一教程中的工作方式相同,with one exception - we're going to create a new binding for each severity we're interested in.

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

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

Putting it all together

 EmitLogDirect.java

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();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        String severity = getSeverity(argv);
        String message = getMessage(argv);

        channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
    }
  }
  //..
}

ReceiveLogsDirect.java

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();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    channel.exchangeDeclare(EXCHANGE_NAME, "direct");
    String queueName = channel.queueDeclare().getQueue();

    if (argv.length < 1) {
        System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
        System.exit(1);
    }

    for (String severity : argv) {
        channel.queueBind(queueName, EXCHANGE_NAME, severity);
    }
    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

上一个教程中,我们改进了 记录系统。而不是使用只能 虚拟广播,我们用了一个,并获得了一种可能性 有选择地接收日志。fanout direct

通过使用direct exchange 提升我们的系统柜,它仍然具有限制-不能基于multiple 条件进行路由

在我们的日志系统中,我们可能不仅要subscribe 订阅日志 基于严重性,但也基于发出日志的源。 您可能从 syslog unix 工具中知道这个概念,该工具 根据严重性(信息/警告/暴击等)和设施路由日志 (auth/cron/kern...)。

这将给我们很大的灵活性——我们可能想听听 只有来自“cron”的严重错误,还有来自“kern”的所有日志。

为了在我们的日志系统中实现这一点,我们需要了解更多 topic exchange

Topic exchange

信息发送到一个topic exchange 不能有任意值routing_key,必须是一个单词列表,但通常它们指定一些特征 连接到消息,一些有效的路由,例如: "stock.usd.nyse", "nyse.vmw","quick.orange.rabbit".可以有 路由键中的多个单词,最多 255 个 字节

binding key也必须采用相同的形式。topic exchange 背后的逻辑类似于一个 - 使用 direct的路由密钥将传递到所有队列 使用匹配的绑定键绑定。但是,有两个重要的 绑定键的特殊情况:topic direct

  • *(星号)可以完全代替一个单词。
  • #(hash) 可以替换零个或多个单词。

在示例中解释这一点是最简单的:

我们创建了三个绑定:Q1 绑定了绑定键 “” Q2 带有 “” 和 “”。*.orange.**.*.rabbitlazy.#

这些绑定可以概括为:

  • Q1 is interested in all the orange animals.
  • Q2 wants to hear everything about rabbits, and everything about lazy animals.

路由键设置为“”的邮件 将传送到两个队列。消息 “”也会去他们俩。另一方面 “” 将只转到第一个队列,并且 “”只到第二个。 仅传递到第二个队列一次,即使它与两个绑定匹配。 “” 与任何绑定都不匹配,因此它将被丢弃。quick.orange.rabbitlazy.orange.elephantquick.orange.foxlazy.brown.foxlazy.pink.rabbitquick.brown.fox

Putting it all together

EmitLogTopic.java

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();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        String routingKey = getRouting(argv);
        String message = getMessage(argv);

        channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
        System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
    }
  }
  //..
}

ReceiveLogsTopic.java

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();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    channel.exchangeDeclare(EXCHANGE_NAME, "topic");
    String queueName = channel.queueDeclare().getQueue();

    if (argv.length < 1) {
        System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
        System.exit(1);
    }

    for (String bindingKey : argv) {
        channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
    }

    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

,我们将使用 RabbitMQ 来构建一个 RPC 系统: 客户端和可伸缩的 RPC 服务器。因为我们没有任何耗时的东西 值得分发的任务,我们将创建一个虚拟 RPC 返回斐波那契数列的服务。

客户端界面

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

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

回调队列

一般来说,在 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 ...

我们需要这个新的导入:

import com.rabbitmq.client.AMQP.BasicProperties;

消息属性

AMQP 0-9-1 协议预定义了一组 14 个属性,这些属性与 一条消息。大多数属性很少使用,但 以下内容:

  • deliveryMode:将消息标记为持久性(值为 ) 或瞬态(任何其他值)。你可能还记得这家酒店 从第二个教程开始。
  • contentType:用于描述编码的 mime 类型。 例如,对于常用的JSON编码,这是一个很好的做法 将此属性设置为: 。application/json
  • replyTo:常用于命名回调队列。
  • correlationId:用于将 RPC 响应与请求相关联。
  • RPC 将按如下方式工作:

  • 对于 RPC 请求,客户端发送一条具有两个属性的消息: ,设置为创建的匿名独占队列 只是为了请求,并且 设置为每个请求的唯一值。replyTocorrelationId
  • 请求将发送到队列。rpc_queue
  • RPC 工作线程(又名:服务器)正在等待该队列上的请求。 当请求出现时,它会完成工作并发送一条消息,其中包含 结果返回给客户端,使用字段中的队列。replyTo
  • 客户端等待应答队列中的数据。当消息 出现时,它会检查属性。如果匹配 请求中的值,它将响应返回给 应用。correlationId

RPCServer.java

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("localhost");

        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");

        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);
            } finally {
                channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };

        channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> {}));
    }
}

RPCClient.java

mport 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.util.UUID;
import java.util.concurrent.*;

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("localhost");

        connection = factory.newConnection();
        channel = connection.createChannel();
    }

    public static void main(String[] argv) {
        try (RPCClient fibonacciRpc = new RPCClient()) {
            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 | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public String call(String message) throws IOException, InterruptedException, ExecutionException {
        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"));

        final CompletableFuture<String> response = new CompletableFuture<>();

        String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                response.complete(new String(delivery.getBody(), "UTF-8"));
            }
        }, consumerTag -> {
        });

        String result = response.get();
        channel.basicCancel(ctag);
        return result;
    }

    public void close() throws IOException {
        connection.close();
    }
}

7. Publisher Confirms

使用发布者确认来使 确定已发布的消息已安全到达代理。我们会的 涵盖使用发布者确认和解释的几种策略 他们的优点和缺点。

Enabling Publisher Confirms on a Channel (在频道上启用发布确认)

发布者确认是 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);
}

我们像往常一样发布一条消息并等待其 用方法确认。 确认消息后,该方法将立即返回。如果 消息在超时内未确认,或者如果它被 nack-ed(意思是 由于某种原因,经纪人无法处理它),该方法将 抛出异常。异常的处理通常包括 在记录错误消息和/或重试发送消息时。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) {
        channel.waitForConfirmsOrDie(5_000);
        outstandingMessageCount = 0;
    }
}
if (outstandingMessageCount > 0) {
    channel.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 个回调:一个用于确认的消息,一个用于 nack-ed 消息 (代理可能认为丢失的消息)。每个回调都有 2 参数:

在发布之前,可以通过以下方式获得序列号: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());

发布代码现在使用地图跟踪出站消息。我们需要 在确认到达时清理此地图并执行诸如记录警告之类的操作 当消息被 nack-ed:

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

前面的示例包含一个回调,该回调在以下情况下清理映射 确认到达。请注意,此回调同时处理单个和多个 证实。当确认到达时,将使用此回调(作为 的第一个参数)。nack-ed 消息的回调 检索邮件正文并发出警告。然后,它重用 上一次回调清理未完成的映射确认(是否 消息已确认或已确认,它们在地图中的对应条目 必须删除。Channel#addConfirmListener

如何跟踪未完成的确认?

我们的样品使用 a 来跟踪未完成的确认。 出于多种原因,这种数据结构很方便。它允许 轻松将序列号与消息相关联(无论消息数据如何) is)并轻松清理条目,直至给定的序列 ID(用于处理 多次确认/唠叨)。最后,它支持并发访问,因为 确认回调是在客户端库拥有的线程中调用的,该线程 应与发布线程保持不同。ConcurrentNavigableMap

除了 复杂的映射实现,例如使用简单的并发哈希映射 以及一个用于跟踪发布序列下限的变量,但 他们通常参与更多,不属于教程。

总而言之,异步处理发布者确认通常需要 以下步骤:

重新发布 nack-ed 消息?

从相应的 回调,但应避免这种情况,因为确认回调是 在不应该有通道的 I/O 线程中调度 进行操作。更好的解决方案是在内存中对消息进行排队 由发布线程轮询的队列。像这样的类是确认回调之间传输消息的一个很好的候选者 和发布线程。ConcurrentLinkedQueue

总结

某些应用程序中,确保已发布的消息到达代理可能是必不可少的。 发布者确认是有助于满足此要求的 RabbitMQ 功能。发行人 确认本质上是异步的,但也可以同步处理它们。 没有明确的方法来实现发布者确认,这通常会下降 应用程序和整个系统中的约束。典型的技术包括:

PublisherConfirms.java

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

}

    • MULTIPLE:这是一个布尔值。如果为 false,则仅确认/编辑一条消息,如果 true,所有序列号较低或相等的消息都将被确认/nack-ed。
    • 提供一种将发布序列号与邮件相关联的方法。
    • 在频道上注册确认监听器,以便在以下情况下收到通知 发布者 ack/nacks 到达以执行适当的操作,例如 记录或重新发布 nack-ed 消息。发送到消息的序列号 在此步骤中,关联机制可能还需要进行一些清理。
    • 在发布邮件之前跟踪发布序列号。
    序列号:标识已确认的数字 或唠叨的消息。我们很快就会看到如何将其与已发布的消息相关联。
    • 单独发布消息,同步等待确认:简单,但非常 吞吐量有限。
    • 批量发布消息,等待批量同步确认:简单、合理 吞吐量,但很难推理何时出现问题。
    • 异步处理:最佳性能和资源使用,在出错时控制良好,但 可以参与正确实施。
  • 参考源码:
  • https://github.com/rabbitmq
  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值