RabbitMQ Java客户端API指南

Java客户端API指南

来自RabbitMQ官网 使用google翻译和百度翻译,还存在错误、歧义、仅供自己方便查阅,如有疑问请查看英文原文

API:https://rabbitmq.github.io/rabbitmq-java-client/api/current/overview-summary.html

API指南:http://www.rabbitmq.com/api-guide.html

* 连接到RabbitMQ
* 连接和通道生命周期
* 使用Exchange交换机和队列
* 发布消息
* 使用订阅消费
* 并发注意事项和安全性
* 从网络故障中自动恢复

该库的5.x版本系列需要JDK 8,用于编译和运行时。在Android上,这意味着仅支持Android 7.0或更高版本。4.x版本系列支持7.0之前的JDK 6和Android版本。

该库是开源的,在GitHub上开发,并获得三种许可

这意味着用户可以认为库是根据上面列表中的任何许可证获得许可的。例如,用户可以选择Apache Public License 2.0并将此客户端包含在商业产品中。根据GPLv2许可的代码库可以选择GPLv2,依此类推。

还有一些命令行工具 曾经随Java客户端一起提供。

客户端API在AMQP 0-9-1协议模型上进行了严格建模,并提供了易于使用的附加抽象。

一个API参考(JavaDoc的)是单独提供的。

概观

RabbitMQ Java客户端使用com.rabbitmq.client作为其顶级包。关键类和接口是:

  • Channel:连接通道。
  • 连接:表示AMQP连接实体
  • ConnectionFactory:构造Connection实例
  • 消费者:代表消息消费者
  • DefaultConsumer:消费者常用的基类
  • BasicProperties:消息属性(元数据)
  • BasicProperties.Builder:生产者BasicProperties

协议操作可通过 Channel接口获得。连接用于打开通道,注册连接生命周期事件处理程序以及关闭不再需要的连接。 Connection通过ConnectionFactory实例化,这是您配置各种连接设置的方式,例如vhost或用户名。

连接和渠道

核心API类是Connection 和Channel,分别代表AMQP 0-9-1连接和通道。它们通常在使用前导入:

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

连接到RabbitMQ

以下代码使用给定参数(主机名,端口号等)连接到RabbitMQ节点:

ConnectionFactory factory = new ConnectionFactory();
// "guest"/"guest" by default, limited to localhost connections
factory.setUsername(userName);//用户名 默认guest
factory.setPassword(password);//密码 默认guest
factory.setVirtualHost(virtualHost);//虚拟主机,主要用于辨别用户与权限 无需求可使用/
factory.setHost(hostName);//主机 本地127.0.0.1
factory.setPort(portNumber);//端口 可使用默认可修改

Connection conn = factory.newConnection(); //创建连接实体

所有这些参数都具有本地运行的RabbitMQ节点的合理默认值。如果在创建连接之前属性仍未分配,则将使用属性的默认值:

属性 默认值
用户名 “guest”
密码 “guest”
虚拟主机 “/”
主机名 “本地主机 或者 127.0.0.1”
港口 5672用于常规连接, 5671用于使用TLS的连接

或者,可以使用URI

ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost");
Connection conn = factory.newConnection();

请注意,默认情况下,用户guest虚拟机只能从localhost连接。这是为了限制生产系统中使用,不安全!

Channel channel = conn.createChannel(); //打开通道,现在可以通过channel发送或者接收数据

在服务器节点日志中 可以观察到成功和不成功的客户端连接事件。

断开与RabbitMQ的连接

要断开连接,只需关闭通道和连接:

channel.close();
conn.close();

请注意,关闭通道可能被视为良好做法,但这里并不是必需的 - 无论如何,当底层连接关闭时,它将自动完成。

可以在服务器节点日志中观察到客户端断开连接事件。

连接和通道寿命

连接意味着长寿。底层协议是为长时间运行的连接而设计和优化的。这意味着每次操作打开一个新连接,例如发布的消息,是不必要的,并且强烈不鼓励,因为它会引入大量的网络往返和开销。

通道也意味着长寿,但由于许多可恢复的协议错误将导致通道关闭,因此通道寿命可能短于其连接的寿命。每次操作关闭和打开新通道通常是不必要的,但可能是合适的。如有疑问,请考虑重复使用通道。

通道级异常(例如尝试从不存在的队列中使用)将导致通道关闭。无法再使用已关闭的通道,也不会再从服务器接收任何事件(例如消息传递)。RabbitMQ将记录通道级异常,并将启动通道的关闭序列(见下文)。

使用Exchange和队列

客户端应用程序使用交换和队列协议的高级构建块。这些必须在使用之前声明。声明任一类型的对象只是确保存在该名称之一,并在必要时创建它。

继续前面的示例,以下代码声明了一个direct交换机和一个服务器命名的队列,然后将它们绑定在一起。

channel.exchangeDeclare(exchangeName, "direct", true);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, routingKey);

这将主动声明以下对象,这两个对象都可以使用其他参数进行自定义。在这里,他们都没有任何特殊的论点。

  1. 一种持久的,非自动交换的“direct”类型的交换机
  2. 具有生成名称的非持久,独占,自动删除的队列

然后,上述函数调用将队列绑定到具有给定路由键的交换机上。

请注意,当只有一个客户端想要使用它时,这将是一种典型的声明队列的方式:它不需要一个众所周知的名称,没有其他客户端可以使用它(独占)并且将自动清除(自动删除) )。如果多个客户端希望共享具有已知名称的队列,则此代码将是合适的:

channel.exchangeDeclare(exchangeName, "direct", true);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);

这表示:

  1. 一种持久的,非自动交换的“direct”类型的交换
  2. 具有已知名称的持久,非独占,非自动删除的队列

许多Channel API方法都被重载了。这些方便的简短形式的exchangeDeclarequeueDeclarequeueBind使用合理的默认值。还有更长的表单和更多参数,让您可以根据需要覆盖这些默认值,在需要时提供完全控制。

这种“简短形式,长形式”模式在整个客户端API都有使用。

被动声明

可以“被动地”声明队列和交换机。被动声明只是检查具有提供名称的实体是否存在。如果是,则操作是无操作。对于队列,成功的被动声明将返回与非被动声明相同的信息,即队列中处于就绪状态的消费者和消息的数量。如果实体不存在,则操作将失败并显示通道级别异常。之后不能使用该通道。应该打开一个新通道。通常使用一次性(临时)通道进行被动声明。

Channel#queueDeclarePassiveChannel#exchangeDeclarePassive是用于被动声明的方法。以下示例演示了Channel#queueDeclarePassive

Queue.DeclareOk response = channel.queueDeclarePassive("queue-name");
// returns the number of messages in Ready state in the queue
response.getMessageCount();
// returns the number of consumers the queue has
response.getConsumerCount();

Channel#exchangeDeclarePassive的返回值不包含任何有用信息。因此,如果方法返回并且没有发生通道异常,则意味着交换确实存在。

具有可选响应的操作

一些常见操作也有“无等待”版本,不会等待服务器响应。例如,要声明队列并指示服务器不发送任何响应,请使用

channel.queueDeclareNoWait(queueName, true, false, false, null);

“无等待”版本更有效但提供更低的安全保证,例如,它们更依赖于心跳机制来检测失败的操作。如有疑问,请从标准版开始。只有在具有高拓扑(队列,绑定)流失的场景中才需要“无等待”版本。

删除实体和清除消息

可以显式删除队列或交换机:

channel.queueDelete("queue-name")

只有在队列为空时才可以删除队列:

channel.queueDelete("queue-name", false, true)

或者如果没有使用(没有任何消费者):

channel.queueDelete("queue-name", true, false)

可以清除的队列(删除所有消息):

channel.queuePurge("queue-name")

发布消息

要将消息发布到交换,请使用Channel.basicPublish,如下所示:

byte[] messageBodyBytes = "Hello, world!".getBytes();
channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);

要进行细粒度的控制,可以使用重载变量来指定强制标志,或者使用预先设置的消息属性发送消息:

channel.basicPublish(exchangeName, routingKey, mandatory,
                     MessageProperties.PERSISTENT_TEXT_PLAIN,
                     messageBodyBytes);

这将发送一条消息,其传递模式为2(持久性),优先级为1,内容类型为“text / plain”。您可以使用Builder类来构建自己的消息属性对象,并提及任意数量的属性,例如:

channel.basicPublish(exchangeName, routingKey,
             new AMQP.BasicProperties.Builder()
               .contentType("text/plain")
               .deliveryMode(2)
               .priority(1)
               .userId("bob")
               .build()),
               messageBodyBytes);

此示例发布带有自定义头的消息:

Map<String, Object> headers = new HashMap<String, Object>();
headers.put("latitude",  51.5252949);
headers.put("longitude", -0.0905493);

channel.basicPublish(exchangeName, routingKey,
             new AMQP.BasicProperties.Builder()
               .headers(headers)
               .build()),
               messageBodyBytes);

此示例发布带有过期的消息:

channel.basicPublish(exchangeName, routingKey,
             new AMQP.BasicProperties.Builder()
               .expiration("60000")
               .build()),
               messageBodyBytes);

我们没有说明这里的所有可能性。

请注意,BasicProperties是自动生成的持有者类AMQP的内部类。

如果资源驱动的警报生效 ,Channel#basicPublish的调用最终将被阻止 。

通道和并发注意事项(线程安全)

根据经验,在线程之间共享Channel实例是值得避免的。应用程序应该更喜欢每个线程使用一个Channel,而不是跨多个线程共享相同的Channel

虽然通道上的某些操作可以安全地同时调用,但有些操作并不会导致错误的线路帧交错,双重确认等等。

在共享信道上并发发布可能导致线路上的帧交错错误,触发连接级协议异常以及代理立即关闭连接。因此,它需要在应用程序代码中进行显式同步(必须在关键部分调用Channel#basicPublish)。线程之间共享通道也会干扰Publisher确认。最好完全避免在共享信道上同时发布,例如通过每个线程使用一个信道。

可以使用通道池来避免在共享通道上进行并发发布:一旦线程完成使用通道,它就会将其返回到池中,使通道可用于另一个线程。可以将通道池视为特定的同步解决方案。建议使用现有的池库而不是自行开发的解决方案。例如,Spring AMQP 带有一个现成的通道池功能。

通道消耗资源,在大多数情况下,应用程序在同一JVM进程中很少需要超过几百个开放通道。如果我们假设应用程序为每个通道都有一个线程(因为不应该同时使用通道),单个JVM的数千个线程已经是可以避免的相当大的开销。此外,一些快速发布者可以轻松地使网络接口和代理节点饱和:发布涉及的工作少于路由,存储和传递消息。

要避免的经典反模式是为每个已发布的消息打开一个通道。频道应该是合理的长寿命,而开放一个新的频道是网络往返,这使得这种模式非常低效。

在一个线程中使用并在共享通道上的另一个线程中发布可能是安全的。

同时调度服务器推送的交付(参见下面的部分),保证每个通道的排序得以保留。调度机制使用java.util.concurrent.ExecutorService,每个连接一个。可以使用 ConnectionFactory#setSharedExecutor setter提供由单个ConnectionFactory生成的所有连接共享的自定义执行程序。

使用手动确认时,重要的是要考虑确认的线程。如果它与接收传递的线程不同(例如,Consumer#handleDelivery 委托传递处理到另一个线程),则将多个参数设置为true进行确认是不安全的,并且将导致双重确认,因此会导致通道级协议异常关闭通道。一次确认一条消息能保证是安全的。

按订阅接收邮件(“推送API”)

import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;

接收消息的最有效方法是使用Consumer 接口设置订阅。然后,消息将在到达时自动传递,而不必明确请求。

在调用与Consumer相关的API方法时 ,个人订阅总是由其消费者标签引用。消费者标签是消费者标识符,可以是客户端生成的,也可以是服务器生成的。要让RabbitMQ生成节点范围的唯一标记,请使用不带消费者标记参数的Channel #basicConsume覆盖,或者为消费者标记传递空字符串,并使用Channel#basicConsume返回的值。消费者标签用于取消消费者。

不同的Consumer实例必须具有不同的使用者标记。强烈建议不要在连接上使用重复的消费者标签,这可能会导致自动连接恢复问题,并在监控消费者时导致监控数据混乱。

实现Consumer的最简单方法是将便捷类DefaultConsumer子类化。可以在basicConsume 调用上传递此子类的对象以设置订阅:

boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "myConsumerTag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             String routingKey = envelope.getRoutingKey();
             String contentType = properties.getContentType();
             long deliveryTag = envelope.getDeliveryTag();
             // (process the message components here ...)
             channel.basicAck(deliveryTag, false);
         }
     });

这里,由于我们指定了autoAck = false,因此必须确认传递给Consumer的消息,最方便的是在handleDelivery 方法中完成,如图所示。

更复杂的消费者需要覆盖更多方法。特别是, 当通道和连接关闭时调用handleShutdownSignal,并且在调用对该Consumer的任何其他回调之前, handleConsumeOk将传递消费者标记。

消费者还可以分别实现 handleCancelOkhandleCancel 方法以通知显式和隐式取消。

您可以使用 Channel.basicCancel明确取消特定的Consumer

channel.basicCancel(consumerTag);

传递消费者标签。

与发布商一样,考虑消费者的并发危害安全也很重要。

消费者的 回调在与实例化其Channel的线程分开的线程池中调度 。这意味着Consumer可以安全地在ConnectionChannel上调用阻塞方法 ,例如 Channel#queueDeclare或 Channel#basicCancel

每个Channel都有自己的调度线程。对于每个 通道一个消费者的最常见用例,这意味着消费者不会占用其他消费者。如果每个通上有多个消费者,请注意长时间运行的消费者可能会阻止向该通道上的 其他消费者发送回执 。

有关并发性和并发性危险安全性的其他主题,请参阅“并发注意事项(线程安全性)”部分。

检索单个消息(“Pull API”)

要显式检索消息,请使用 Channel.basicGet。返回的值是GetResponse的一个实例,从中可以提取头信息(属性)和消息体:

boolean autoAck = false;
GetResponse response = channel.basicGet(queueName, autoAck);
if (response == null) {
    // No message retrieved.
} else {
    AMQP.BasicProperties props = response.getProps();
    byte[] body = response.getBody();
    long deliveryTag = response.getEnvelope().getDeliveryTag();
    ...

并且由于上面的autoAck = false,您还必须调用Channel.basicAck以确认您已成功收到消息:

    ...
    channel.basicAck(method.deliveryTag, false); // acknowledge receipt of the message
}

处理不可路由的消息

如果发布的消息设置了“强制”标志,但无法路由,则代理会将其返回给发送客户端(通过AMQP.Basic.Return 命令)。

为了通知这样的回执,客户可以实现ReturnListener 接口并调用Channel.addReturnListener。如果客户端尚未为特定通道配置返回侦听器,则将以静默方式删除关联的返回消息。

例如,如果客户端发布一条消息,其中“mandatory”标志设置为未绑定到队列的“direct”类型的交换机,则将调用返回侦听器。

channel.addReturnListener(new ReturnListener() {
    public void handleReturn(int replyCode,
                                  String replyText,
                                  String exchange,
                                  String routingKey,
                                  AMQP.BasicProperties properties,
                                  byte[] body)
    throws IOException {
        ...
    }
})

关机协议

客户端关闭过程概述

AMQP 0-9-1连接和通道共享管理网络故障,内部故障和显式本地关闭的相同通用方法。

AMQP 0-9-1连接和通道具有以下生命周期状态:

  • 打开:对象已准备好使用
  • 关闭:已明确通知对象本地关闭,已向任何支持的下层对象发出关闭请求,并正在等待其关闭过程完成
  • 已关闭:对象已从任何较低层对象收到所有关闭完成通知,因此已关闭自身

无论导致关闭的原因如何,这些对象总是处于关闭状态,如应用程序请求,内部客户端库故障,远程网络请求或网络故障。

AMQP连接和通道对象具有以下与关闭相关的方法:

  • addShutdownListener(ShutdownListener listener)和 removeShutdownListener(ShutdownListener listener),用于管理任何监听器,当对象转换为关闭状态时将触发这些听器 。请注意,将ShutdownListener添加到已关闭的对象将立即触发监听器
  • getCloseReason(),允许调查对象关闭的原因是什么
  • isOpen(),用于测试对象是否处于打开状态
  • close(int closeCode,String closeMessage),显式通知对象关闭

监听器的简单用法如下所示:

import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.client.ShutdownListener;

connection.addShutdownListener(new ShutdownListener() {
    public void shutdownCompleted(ShutdownSignalException cause)
    {
        ...
    }
});

有关关闭情况的信息

可以通过显式调用getCloseReason() 方法或使用ShutdownListener类的服务(ShutdownSignalException cause) 方法中的cause参数来检索 ShutdownSignalException,其中包含有关close原因的所有可用信息。

ShutdownSignalException类提供方法来分析关机的原因。通过调用isHardError()方法,我们可以获得有关连接或通道错误的信息,并且getReason()以AMQP方法的形式返回有关原因的信息 -AMQP.Channel.Close或 AMQP.Connection.Close(如果原因是库中的某些异常,则返回null,例如网络通信失败,在这种情况下,可以使用getCause()检索异常。

public void shutdownCompleted(ShutdownSignalException cause)
{
  if (cause.isHardError())
  {
    Connection conn = (Connection)cause.getReference();
    if (!cause.isInitiatedByApplication())
    {
      Method reason = cause.getReason();
      ...
    }
    ...
  } else {
    Channel ch = (Channel)cause.getReference();
    ...
  }
}

原子性和使用isOpen()方法

不建议对生产代码 使用通道和连接对象的isOpen()方法,因为方法返回的值取决于是否存在关闭原因。以下代码说明了竞争条件的可能性:

public void brokenMethod(Channel channel)
{
    if (channel.isOpen())
    {
       //下面的代码取决于信道处于打开状态。

        //然而,信道状态的可能是变化的。

        //在ISOPEN()和Basic QoS(1)调用之间
        ...
        channel.basicQos(1);
    }
}

相反,我们通常应该忽略这种检查,并简单地尝试所需的操作。如果在执行代码期间关闭了连接的通道,则将抛出ShutdownSignalException,指示对象处于无效状态。我们还应该捕获 由于SocketException导致的IOException,当代理意外关闭连接时,或者 当代理启动清理关闭时ShutdownSignalException

public void validMethod(Channel channel)
{
    try {
        ...
        channel.basicQos(1);
    } catch (ShutdownSignalException sse) {
        // possibly check if channel was closed
        // by the time we started action and reasons for
        // closing it
        ...
    } catch (IOException ioe) {
        // check why connection was closed
        ...
    }
}

高级连接选项

消费者线程池

默认情况下,消费者线程(请参阅下面的接收)在新的ExecutorService线程池中自动分配。如果需要更大的控制,请在newConnection()方法上 提供ExecutorService,以便使用此线程池。这是一个提供比通常分配的线程池更大的线程池的示例:

 ExecutorService es = Executors.newFixedThreadPool(20);
  Connection conn = factory.newConnection(es);

它们都是java.util.concurrent包中的。

当连接被关闭,默认的ExecutorService 将关闭 ,但一个用户提供 的ExecutorService(如ES上文)将 不会被关闭() 。提供自定义ExecutorService的客户端必须确保最终关闭(通过调用其shutdown 方法),否则池的线程可能会阻止JVM终止。

相同的执行程序服务可以在多个连接之间共享,或者在重新连接时串行重用,但在shutdown()之后不能使用 。

只有在有证据表明消费者 回调处理存在严重瓶颈时,才应考虑使用此功能。如果没有执行Consumer回调,或者很少,则默认分配就足够了。开销最初是最小的,并且分配的总线程资源是有界的,即使偶尔会发生消费者活动的爆发。

使用主机列表

可以将Address数组传递给newConnection()地址是简单地在一个简便的类,在com.rabbitmq.client包,包括主机 和端口组件。例如:

Address[] addrArr = new Address[]{ new Address(hostname1, portnumber1)
                                   , new Address(hostname2, portnumber2)};
  Connection conn = factory.newConnection(addrArr);

将尝试连接到hostname1:portnumber1,如果无法连接到hostname2:portnumber2。返回的连接是数组中第一个成功的连接(不抛出 IOException)。这完全等同于在工厂上重复设置主机和端口,每次调用factory.newConnection(),直到其中一个成功。

如果还提供了ExecutorService(使用表单factory.newConnection(es,addrArr)),则线程池与(第一个)成功连接相关联。

如果您希望更多地控制要连接的主机,请参阅 服务发现支持

使用AddressResolver接口进行服务发现

从版本3.6.6开始,可以让AddressResolver的实现 选择创建连接时的连接位置:

Connection conn = factory.newConnection(addressResolver);

AddressResolver界面是这样的:

public interface AddressResolver {

    List<Address> getAddresses() throws IOException;

  }

就像主机列表一样,首先尝试返回的第一个地址,如果客户端无法连接到第一个地址则返回第二个地址,依此类推。

如果还提供了ExecutorService(使用表单factory.newConnection(es,addressResolver)),则线程池与(第一个)成功连接相关联。

AddressResolver是实现定制服务发现逻辑,这是一个动态的基础设施特别有用的理想场所。结合自动恢复,客户端可以自动连接到第一次启动时甚至没有启动的节点。亲和性和负载平衡是自定义AddressResolver可能有用的其他方案。

Java客户端附带以下实现(有关详细信息,请参阅javadoc):

  1. DnsRecordIpAddressResolver:给定主机的名称,返回其IP地址(针对平台DNS服务器的解决方案)。这对于简单的基于DNS的负载平衡或故障转移非常有用。
  2. DnsSrvRecordAddressResolver:给定服务名称,返回主机名/端口对。搜索实现为DNS SRV请求。当使用像HashiCorp Consul这样的服务注册表时,这可能很有用 。

心跳超时

有关心跳以及如何在Java客户端中配置心跳的详细信息,请参阅心跳指南

自定义线程工厂

Google App Engine(GAE)等环境可以限制直接线程实例化。要在这样的环境中使用RabbitMQ Java客户端,必须配置一个自定义ThreadFactory,它使用适当的方法来实例化线程,例如GAE的ThreadManager。以下是Google App Engine的示例。

import com.google.appengine.api.ThreadManager;

  ConnectionFactory cf = new ConnectionFactory();
  cf.setThreadFactory(ThreadManager.backgroundThreadFactory());

支持Java非阻塞IO

Java客户端4.0版为Java非阻塞IO(又名Java NIO)带来了实验性支持。NIO不应该比阻塞IO更快,它只是允许更容易地控制资源(在这种情况下:线程)。

使用默认阻塞IO模式,每个连接都使用一个线程从网络套接字读取。使用NIO模式,您可以控制从/向网络套接字读取和写入的线程数。

如果Java进程使用多个连接(数十个或数百个),请使用NIO模式。您应该使用比默认阻塞模式更少的线程。如果设置了适当数量的线程,则不应尝试降低性能,尤其是在连接不那么繁忙的情况下。

必须明确启用NIO:

  ConnectionFactory connectionFactory = new ConnectionFactory();
  connectionFactory.useNio();

可以通过NioParams类配置NIO模式:

 connectionFactory.setNioParams(new NioParams().setNbIoThreads(4));

NIO模式使用合理的默认值,但您可能需要根据自己的工作负载进行更改。一些设置是:使用的IO线程总数,缓冲区大小,用于IO循环的服务执行程序,内存中写入队列的参数(写入请求在网络上发送之前排队)。有关详细信息和默认值,请阅读Javadoc。

从网络故障中自动恢复

连接恢复

客户端和RabbitMQ节点之间的网络连接可能会失败。RabbitMQ Java客户端支持自动恢复连接和拓扑(队列,交换,绑定和使用者)。许多应用程序的自动恢复过程遵循以下步骤:

  1. 重新连接
  2. 恢复连接监听器
  3. 重新开放通道
  4. 恢复频道监听器
  5. 恢复通道basic.qos设置,发布者确认和处理设置

拓扑恢复包括针对每个通道执行的以下操作

  1. 重新声明交换(预定义的交换机除外)
  2. 重新声明队列
  3. 恢复所有绑定
  4. 恢复所有消费者

从Java客户端4.0.0版开始,默认情况下会启用自动恢复(因此也会启用拓扑恢复)

拓扑恢复依赖于实体的每个连接缓存(队列,交换,绑定,使用者)。例如,当在连接上声明队列时,它将被添加到缓存中。当它被删除或被安排删除时(例如因为它被自动删除),它将被删除。此模型有一些限制如下。

要禁用或启用自动连接恢复,请使用factory.setAutomaticRecoveryEnabled(boolean) 方法。以下代码段显示了如何显式启用自动恢复(例如,对于Java客户端之前的4.0.0):

​
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setHost(hostName);
factory.setPort(portNumber);
factory.setAutomaticRecoveryEnabled(true);
// connection that will recover automatically
Connection conn = factory.newConnection();

如果由于异常导致恢复失败(例如,RabbitMQ节点仍然无法访问),则会在固定的时间间隔(默认为5秒)后重试。间隔可以配置:

​
ConnectionFactory factory = new ConnectionFactory();
// attempt recovery every 10 seconds
factory.setNetworkRecoveryInterval(10000);

当提供地址列表时,列表被洗牌并且所有地址都被尝试,一个接一个地尝试:

ConnectionFactory factory = new ConnectionFactory();

Address[] addresses = {new Address("192.168.1.4"), new Address("192.168.1.5")};
factory.newConnection(addresses);

何时会触发连接恢复?

自动连接恢复(如果已启用)将由以下事件触发:

  • 连接的I / O循环中抛出I / O异常
  • 套接字读取操作超时
  • 检测到错过的服务器心跳
  • 连接的I / O循环中抛出任何其他意外异常

以先发生者为准。

如果与RabbitMQ节点的初始客户端连接失败,则无法启动自动连接恢复。应用程序开发人员负责重试此类连接,记录失败尝试,实现重试次数限制等。这是一个非常基本的例子:

ConnectionFactory factory = new ConnectionFactory();
// configure various connection settings

try {
  Connection conn = factory.newConnection();
} catch (java.net.ConnectException e) {
  Thread.sleep(5000);
  // apply retry logic
}

当应用程序通过Connection.Close方法关闭连接时,将不会启动连接恢复。

通道级异常不会触发任何类型的恢复,因为它们通常指示应用程序中的语义问题(例如,尝试从不存在的队列中消耗)。

恢复监听

可以在可恢复的连接和通道上注册一个或多个恢复侦听器。启用连接恢复后,ConnectionFactory#newConnectionConnection#createChannel返回的 连接 实现com.rabbitmq.client.Recoverable,提供两个具有相当描述性名称的方法:

  • addRecoveryListener
  • removeRecoveryListener

请注意,您目前需要将连接和通道强制转换可恢复 ,以便使用这些方法。

对发布的影响

连接断开时 使用Channel.basicPublish发布的消息将丢失。连接恢复后,客户端不会将它们排入队列。为确保已发布的消息到达RabbitMQ,应用程序需要使用Publisher Confirms 和帐户进行连接失败。

拓扑恢复

拓扑恢复涉及交换,队列,绑定和消费者的恢复。启用自动恢复时,默认情况下会启用它。因此,从Java客户端4.0.0开始默认启用拓扑恢复

如果需要,可以显式禁用拓扑恢复:

ConnectionFactory factory = new ConnectionFactory();

Connection conn = factory.newConnection();
// enable automatic recovery (e.g. Java client prior 4.0.0)
factory.setAutomaticRecoveryEnabled(true);
// disable topology recovery
factory.setTopologyRecoveryEnabled(false);
          

故障检测和恢复限制

自动连接恢复具有许多限制和有意的设计决策,应用程序开发人员需要了解这些限制。

拓扑恢复依赖于实体的每个连接缓存(队列,交换机,绑定,消费者)。例如,当在连接上声明队列时,它将被添加到缓存中。当它被删除或被安排删除时(例如因为它被自动删除),它将被删除。这使得可以在不同通道上声明和删除实体而不会产生意外结果。它还意味着消费者标签(特定于通道的标识符)在使用自动连接恢复的连接上的所有通道中必须是唯一的。

当连接断开或丢失时,检测需要时间。因此,存在一个时间窗口,其中库和应用程序都不知道有效的连接失败。在此时间范围内发布的任何消息都会被序列化并像往常一样写入TCP套接字。只有通过发布商确认才能保证向代理商的交付:AMQP 0-9-1中的发布完全是异步设计。

当启用自动恢复的连接检测到套接字或I / O操作错误时,恢复在可配置延迟后开始,默认情况下为5秒。这种设计假设即使很多网络故障是短暂的并且通常是短暂的,它们也不会立即消失。连接恢复尝试将以相同的时间间隔继续,直到成功打开新连接。

当连接处于恢复状态时,任何在其通道上尝试的发布都将被拒绝并发生异常。客户端当前不对此类传出消息执行任何内部缓冲。应用程序开发人员有责任跟踪此类消息,并在恢复成功时重新发布这些消息。发布者确认是一种协议扩展,应由无法承受邮件丢失的发布者使用。

由于通道级异常,当通道关闭时,连接恢复不会启动。此类异常通常表示应用程序级别的问题。库无法做出明智的决定。

即使在连接恢复启动后,也不会恢复已关闭的通道。这包括明确关闭的通道和上面的通道级异常情况。

手动确认和自动恢复

使用手动确认时,与RabbitMQ节点的网络连接可能在消息传递和确认之间失败。连接恢复后,RabbitMQ将重置所有通道上的交付标签。这意味着 带有旧传递标记的basic.ackbasic.nackbasic.reject将导致通道异常。为避免这种情况,RabbitMQ Java客户端会跟踪和更新传递标记,以使它们在恢复之间单调增长。 Channel.basicAck, Channel.basicNack和 Channel.basicReject然后将调整后的交付标签转换为RabbitMQ使用的标签。将不会发送包含陈旧交货标签的致谢。使用手动确认和自动恢复的应用程序必须能够处理重新发送。

渠道生命周期和拓扑恢复

自动连接恢复对应用程序开发人员来说应尽可能透明,这就是为什么即使多个连接失败并在后台恢复,Channel实例仍保持不变。从技术上讲,当启用自动恢复时,Channel实例充当代理或装饰器:它们将AMQP业务委托给实际的AMQP通道实现,并围绕它实现一些恢复机制。这就是为什么在创建一些资源(队列,交换,绑定)之后不应该关闭某个通道,或者这些资源的拓扑恢复将在以后失败,因为该通道已关闭。相反,在应用程序的生命周期中保持创建通道。

未处理的例外情况

与连接,通道,恢复和使用者生命周期相关的未处理异常被委托给异常处理程序。异常处理程序是实现ExceptionHandler接口的任何对象 。默认情况下,使用DefaultExceptionHandler的实例。它将异常详细信息打印到标准输出。

可以使用ConnectionFactory#setExceptionHandler覆盖处理程序 。它将用于工厂创建的所有连接:

ConnectionFactory factory = new ConnectionFactory();
cf.setExceptionHandler(customHandler);

异常处理程序应该用于异常日志记录。

度量和监控

从版本4.0.0开始,客户端收集运行时指标(例如已发布消息的数量)。度量标准集合是可选的,使用setMetricsCollector(metricsCollector)方法在ConnectionFactory级别 设置。此方法需要MetricsCollector实例,该实例在客户端代码的多个位置调用。

客户端支持 Micrometer(从4.3版本开始)和 Dropwizard Metrics 开箱即用。

以下是收集的指标:

  • 打开的连接数
  • 开放频道数量
  • 已发布消息的数量
  • 消费消息数量
  • 已确认消息的数量
  • 被拒绝的消息数量

Micrometer和Dropwizard Metrics都提供与消息相关的指标的计数,但也包括平均速率,最后五分钟速率等。它们还支持用于监视和报告的常用工具(JMX,Graphite,Ganglia,Datadog等)。有关详细信息,请参阅下面的专用部分。

请注意有关指标收集的以下内容:

  • 在使用Micrometer或Dropwizard Metrics时,不要忘记将相应的依赖项(在Maven,Gradle或甚至作为JAR文件中)添加到JVM类路径。这些是可选的依赖项,不会使用Java客户端自动提取。您可能还需要根据使用的报告后端添加其他依赖项。
  • 度量标准集合是可扩展的。鼓励为特定需求实现自定义 MetricsCollector
  • 所述MetricsCollector设置在连接工厂 水平,但可以在不同的实例共享。
  • 度量标准收集不支持事务。例如,如果在事务中发送确认并且然后回滚事务,则在客户端度量中计数确认(但显然不是由代理计算)。请注意,确认实际上已发送到代理,然后由事务回滚取消,因此客户端度量标准在发送的确认方面是正确的。总而言之,不要将客户端指标用于关键业务逻辑,但不能保证它们完全准确。它们旨在用于简化有关正在运行的系统的推理并使操作更有效。

千分尺支持

您可以通过以下方式使用微米级的指标收集 :

ConnectionFactory connectionFactory = new ConnectionFactory();
MicrometerMetricsCollector metrics = new MicrometerMetricsCollector();
connectionFactory.setMetricsCollector(metrics);
...
metrics.getPublishedMessages(); // get Micrometer's Counter object

​

Micrometer支持 多种报告后端:Netflix Atlas,Prometheus,Datadog,Influx,JMX等。

您通常会将MeterRegistry的实例传递 给MicrometerMetricsCollector。以下是JMX的示例:

JmxMeterRegistry registry = new JmxMeterRegistry();
MicrometerMetricsCollector metrics = new MicrometerMetricsCollector(registry);
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setMetricsCollector(metrics);

Dropwizard指标支持

您可以通过以下方式使用Dropwizard启用指标收集 :

ConnectionFactory connectionFactory = new ConnectionFactory();
StandardMetricsCollector metrics = new StandardMetricsCollector();
connectionFactory.setMetricsCollector(metrics);
...
metrics.getPublishedMessages(); // get Metrics' Meter object

​

Dropwizard Metrics支持 多种报告后端:console,JMX,HTTP,Graphite,Ganglia等。

您通常会将MetricsRegistry的实例传递 给StandardMetricsCollector。以下是JMX的示例:

MetricRegistry registry = new MetricRegistry();
StandardMetricsCollector metrics = new StandardMetricsCollector(registry);

ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setMetricsCollector(metrics);

JmxReporter reporter = JmxReporter
  .forRegistry(registry)
  .inDomain("com.rabbitmq.client.jmx")
  .build();
reporter.start();

​

Google App Engine上的RabbitMQ Java客户端

在Google App Engine(GAE)上使用RabbitMQ Java客户端需要使用自定义线程工厂,该工厂使用GAE的ThreadManager实例化线程(参见上文)。此外,有必要设置一个低心跳间隔(4-5秒),以避免在GAE上遇到低InputStream读取超时:

ConnectionFactory factory = new ConnectionFactory();
cf.setRequestedHeartbeat(5);

警告和限制

为了实现拓扑恢复,RabbitMQ Java客户端维护着已声明的队列,交换和绑定的缓存。缓存是每个连接。某些RabbitMQ功能使客户端无法观察到某些拓扑更改,例如,当由于TTL而删除队列时。RabbitMQ Java客户端尝试在最常见的情况下使缓存条目无效:

  • 删除队列时。
  • 删除交换时。
  • 删除绑定时。
  • 在自动删除的队列上取消使用者时。
  • 当队列或交换机从自动删除的交换中解除绑定时。

但是,客户端无法在单个连接之外跟踪这些拓扑更改。依赖于自动删除队列或交换机的应用程序,以及队列TTL(注意:不是消息TTL!),并使用自动连接恢复,应明确删除已知未使用或已删除的实体,以清除客户端拓扑缓存。这可以通过Channel#queueDelete, Channel#exchangeDeleteChannel#queueUnbindChannel#exchangeUnbind 在RabbitMQ 3.3.x中是幂等的(删除不存在的异常)。

RPC(请求/回复)模式:示例

为了方便编程,Java客户端API提供了一个类RpcClient,它使用临时回复队列通过AMQP 0-9-1 提供简单的RPC样式通信工具。

该类不会对RPC参数和返回值强加任何特定格式。它只是提供了一种机制,用于使用特定路由密钥向给定交换机发送消息,并等待应答队列上的响应。

import com.rabbitmq.client.RpcClient;

RpcClient rpc = new RpcClient(channel, exchangeName, routingKey);

(此类如何使用AMQP 0-9-1的实现细节如下:请求消息是在 basic.correlation_id字段设置为此RpcClient实例的唯一值的情况下发送的,并且basic.reply_to设置为该名称的回复队列。)

创建此类的实例后,可以使用它通过以下任何方法发送RPC请求:

byte[] primitiveCall(byte[] message);
String stringCall(String message)
Map mapCall(Map message)
Map mapCall(Object[] keyValuePairs)

primitiveCall方法传送原始字节数组作为请求和响应机构。该方法stringCall是围绕一个便利的包装primitiveCall,处理所述消息正文字符串在默认的字符编码的实例。

mapCall变种是有点更复杂的:它们编码java.util.Map包含普通的Java值到AMQP 0-9-1二进制表表示,和解码以同样的方式回应。(请注意,这里可以使用哪些值类型有一些限制 - 有关详细信息,请参阅javadoc。)

所有的编组/解组方便方法都使用primitiveCall作为传输机制,并在其上面提供一个包装层。

TLS支持

可以使用TLS加密客户端和代理之间的通信 。还支持客户端和服务器身份验证(也称为对等验证)。以下是使用Java客户端加密的最简单方法:

ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5671);

factory.useSslProtocol();

请注意,客户端不会在上面的示例中强制执行任何服务器身份验证(对等证书链验证)作为默认值,使用“信任所有证书” TrustManager。这对于本地开发很方便,但容易发生中间人攻击,因此不建议用于生产。要了解有关RabbitMQ中TLS支持的更多信息,请参阅TLS指南。如果您只想配置Java客户端(尤其是对等验证和信任管理器部分),请阅读TLS指南的相应部分

猜你喜欢

转载自blog.csdn.net/qq_34446485/article/details/81327789