本指南涵盖RabbitMQ Java客户端及其公共API。它假定使用了客户端的最新主要版本,并且读者熟悉基础知识。指南的关键部分是:
该库的5.x版本系列需要JDK 8,用于编译和运行时。在Android上,这意味着仅支持Android 7.0或更高版本。4.x版本系列支持JDK 6和Android7.0之前的版本。
该库是开源的,在GitHub上开发,并获得三重许可
这意味着用户可以认为库是根据上面列表中的任何许可证下获得的许可。例如,用户可以选择Apache Public License 2.0并将此客户端包含在商业产品中。根据GPLv2许可的代码库可以选择GPLv2,依此类推。
还有一些命令行工具 随Java客户端一起提供。
客户端API在AMQP 0-9-1协议模型上进行了严格建模,并提供了易于使用的附加抽象。
一个API参考(JavaDoc的)是单独提供的。
RabbitMQ Java客户端使用com.rabbitmq.client作为其顶级包。关键类和接口是:
协议操作可通过 Channel接口获得。连接用于打开频道,注册连接生命周期事件处理程序,以及关闭不再需要的连接。 Connection通过ConnectionFactory实例化,这是您配置各种连接设置的方式,例如vhost或用户名。
核心API类是Connection 和Channel,分别代表AMQP 0-9-1连接和频道。它们通常在使用前导入:
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
以下代码使用给定参数(主机名,端口号等)连接到RabbitMQ节点:
ConnectionFactory factory = new ConnectionFactory();
// "guest"/"guest" by default, limited to localhost connections
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setHost(hostName);
factory.setPort(portNumber);
Connection conn = factory.newConnection();
所有这些参数都具有连接本地运行的RabbitMQ节点的合理默认值。如果在创建连接之前属性仍未分配,则将使用属性的默认值:
属性 | 默认值 |
用户名 | “guest” |
密码 | “guest” |
虚拟主机 | “/” |
主机名 | “localhost” |
端口 | 5672用于常规连接, 5671用于使用TLS的连接 |
或者,可以使用URI:
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost");
Connection conn = factory.newConnection();
所有这些参数都具有连接本地运行RabbitMQ服务器的合理默认值。
请注意,默认情况下,用户guest只能从localhost连接。这是为了限制生产系统中众所周知的凭证使用。
然后,连接接口可以用于打开一个频道:
Channel channel = conn.createChannel();
现在可以使用该频道发送和接收消息,如后续章节中所述。
可以在服务器节点日志中观察到成功和不成功的客户端连接事件。
要断开连接,只需关闭频道和连接:
channel.close();
conn.close();
请注意,关闭频道可能被视为良好做法,但这里并不是必需的 - 无论如何,当底层连接关闭时,它将自动完成。
可以在服务器节点日志中观察到客户端断开连接事件。
连接意味着长寿。底层协议是为长时间运行的连接而设计和优化的。这意味着每次操作打开一个新连接(例如发布的消息)是不必要的,并且强烈建议不要这样做,因为它会引入大量的网络往返和开销。
频道也意味着长寿,但由于许多可恢复的协议错误将导致频道关闭,因此频道寿命可能短于其所属连接的寿命。每次操作关闭和打开新频道通常是不必要的,但可能是合适的。如有疑问,请考虑先重新使用频道。
频道级异常(例如尝试从不存在的队列中消费)将导致频道关闭。无法再使用已关闭的频道,也不会再从服务器接收任何事件(例如消息传递)。RabbitMQ将记录频道级异常,并将启动频道的关闭序列(见下文)。
客户端应用程序使用交换和队列,协议的高级构建块。这些必须在使用之前声明。声明任一类型的对象只是确保存在该名称之一,并在必要时创建它。
继续前面的示例,以下代码声明了一个交换和一个服务器命名的队列,然后将它们绑定在一起。
channel.exchangeDeclare(exchangeName, "direct", true);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, routingKey);
这将主动声明以下对象,这两个对象都可以使用其他参数进行自定义。在这里,他们都没有任何特殊的参数。
然后,上述函数调用将队列绑定到具有给定路由键的交换器。
请注意,当只有一个客户端想要使用它时,这将是一种典型的声明队列的方式:它不需要一个众所周知的名称,没有其他客户端可以使用它(独占)并且将自动清除(自动删除) )。如果多个客户端想要共享具有已知名称的队列,则此代码将是合适的:
channel.exchangeDeclare(exchangeName, "direct", true);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
这将积极宣布:
许多Channel API方法都被重载了。这些方便的简短形式的exchangeDeclare,queueDeclare和queueBind 使用合理的默认值。还有更长的形式和更多参数,让您可以根据需要覆盖这些默认值,在需要时提供完全控制。
这种“简短形式,长形式”模式在整个客户端API中使用。
队列和交换器可以被动地声明。被动声明只是检查具有提供名称的实体是否存在。如果是,则操作是无操作。对于队列,成功的被动声明将返回与非被动声明相同的信息,即队列中处于就绪状态的消费者和消息的数量。如果实体不存在,则操作将失败并显示频道级别异常。之后不能使用该频道。应该打开一个新频道。通常使用一次性(临时)频道进行被动声明。
Channel#queueDeclarePassive和Channel#exchangeDeclarePassive是用于被动声明的方法。以下示例演示了Channel#queueDeclarePassive:
Queue.DeclareOk response = channel.queueDeclarePassive(“queue-name”);
//返回队列中处于Ready状态的消息数
response.getMessageCount();
//返回队列拥有的消费者数量
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 headers = new HashMap();
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 Confirms。最好完全避免在共享信道上同时发布,例如通过每个线程使用一个信道。
可以使用频道池来避免在共享频道上进行并发发布:一旦线程完成使用频道,它就会将其返回到池中,使频道可用于另一个线程。可以将频道池视为特定的同步解决方案。建议使用现有的池库而不是自行开发的解决方案。例如,Spring AMQP 具有即用型频道池功能。
频道消耗资源,在大多数情况下,应用程序在同一JVM进程中很少需要超过几百个开放频道。如果我们假设应用程序为每个频道都有一个线程(因为不应该同时使用频道),单个JVM的数千个线程已经是可以避免的相当大的开销。此外,一些快速发布者可以轻松地使网络接口和代理节点饱和:发布涉及的工作少于路由,存储和传递消息。
要避免的经典反模式是为每个已发布的消息打开一个频道。频道应该是合理的长寿命,而开辟一个新频道是一个网络往返,这使得这种模式非常低效。
在一个线程中消费并在共享频道上的另一个线程中发布可能是安全的。
调度服务器推送的交付(参见下面的部分),同时保证每个频道的排序得以保留。调度机制使用java.util.concurrent.ExecutorService,每个连接一个。可以使用 ConnectionFactory#setSharedExecutorsetter提供由单个ConnectionFactory生成的所有连接共享的自定义执行程序。
使用手动确认时,重要的是要考虑线程的确认。如果它与接收传递的线程不同(例如,Consumer#handleDelivery 委托传递处理到另一个线程),则将multiple参数设置为true进行确认是不安全的,并且将导致双重确认,因此会导致频道级协议异常关闭频道。一次确认一条消息可能是安全的。
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
接收消息的最有效方法是使用Consumer 接口设置订阅。然后,消息将在到达时自动传递,而不是必须明确请求。
在调用与Consumer相关的API方法时 ,个人订阅总是由其消费者标签引用。消费者标签是消费者标识符,可以是客户端或服务器生成的。要让RabbitMQ生成节点范围的唯一标记,请使用不带消费者标记参数的Channel #basicConsume覆盖,或者为消费者标记传递空字符串,并使用Channel#basicConsume返回的值。消费者标签用于取消消费者。
不同的消费者实例必须具有不同的消费者标签。强烈建议不要在连接上使用重复的消费者标签,这会导致自动连接恢复问题,并在监控消费者时导致监控数据混乱。
实现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将传递消费者标记。
消费者还可以分别实现 handleCancelOk和handleCancel 方法以通知显式和隐式取消。
您可以使用 Channel.basicCancel明确取消特定的Consumer:
channel.basicCancel(consumerTag);
传递消费者标签。
与发布着一样,考虑消费者的并发危害安全也很重要。
消费者的 回调在与实例化其Channel的线程分开的线程池中调度 。这意味着Consumer可以安全地在Connection或Channel上调用阻塞方法 ,例如 Channel#queueDeclare或 Channel#basicCancel。
每个Channel都有自己的调度线程。对于每个 频道一个消费者的最常见用例,这意味着消费者不会占用其他消费者。如果每个频道有多个 消费者,请注意长时间运行的消费者可能会阻止向该频道上的 其他消费者发送回调。
有关并发性和并发性危险安全性的其他主题,请参阅“并发注意事项(线程安全性)”部分。
要显式检索消息,请使用 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。如果客户端尚未为特定频道配置返回侦听器,则将以静默方式删除关联的返回消息。
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode,
String replyText,
String exchange,
String routingKey,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
...
}
});
例如,如果客户端发布了了一个标志为“mandatory”的消息到一个“direct”类型的交换上,而这个交换器却没有绑定到队列,则将调用返回侦听器。
AMQP 0-9-1连接和频道共享管理网络故障,内部故障和显式本地关闭的相同通用方法。
AMQP 0-9-1连接和频道具有以下生命周期状态:
无论导致关闭的原因如何,这些对象总是处于关闭状态,如应用程序请求,内部客户端库故障,远程网络请求或网络故障。
AMQP连接和频道对象具有以下与关闭相关的方法:
监听器的简单用法如下所示:
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()方法,因为方法返回的值取决于是否存在关闭原因。以下代码说明了竞争条件的可能性:
public void brokenMethod(Channel channel)
{
if (channel.isOpen())
{
// The following code depends on the channel being in open state.
// However there is a possibility of the change in the channel state
// between isOpen() and basicQos(1) call
...
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);
无论执行人及的ExecutorService类中的java.util.concurrent包。
当连接被关闭的默认的ExecutorService 将关闭() ,但一个用户提供 的ExecutorService(如es上文)将不被关断() 。提供自定义ExecutorService的客户端必须确保最终关闭(通过调用其shutdown() 方法),否则池的线程可能会阻止JVM终止。
相同的执行程序服务可以在多个连接之间共享,或者在重新连接时串行重用,但在shutdown()之后不能使用它 。
只有在有证据表明消费者 回调处理存在严重瓶颈时,才应考虑使用此功能。如果没有执行消费者回调,或者很少,则默认分配绰绰有余。即使偶尔会发生消费者活动的爆发,开销最初也是最小的并且分配的总线程资源是有界的。
可以将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)),则线程池与(第一个)成功连接相关联。
如果您希望更多地控制要连接的主机,请参阅 服务发现支持。
从版本3.6.6开始,可以让AddressResolver的实现 选择创建连接时的连接位置:
Connection conn = factory.newConnection(addressResolver);
该AddressResolver界面是这样的:
public interface AddressResolver {
List getAddresses() throws IOException;
}
就像使用主机列表一样,首先尝试返回的第一个地址,如果客户端无法连接到第一个地址则返回第二个地址,依此类推。
如果还提供了ExecutorService(使用表单factory.newConnection(es,addressResolver)),则线程池与(第一个)成功连接相关联。
该AddressResolver是实现定制服务发现逻辑,这是一个动态的基础设施特别有用的理想场所。结合自动恢复,客户端可以自动连接到第一次启动时甚至没有启动的节点。亲和性和负载平衡是自定义AddressResolver可能有用的其他方案。
Java客户端附带以下实现(有关详细信息,请参阅javadoc):
有关心跳以及如何在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客户端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客户端支持自动恢复连接和拓扑(队列,交换,绑定和使用者)。许多应用程序的自动恢复过程遵循以下步骤:
拓扑恢复包括针对每个频道执行的以下操作
从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);
自动连接恢复(如果已启用)将由以下事件触发:
以先发生者为准。
如果与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#newConnection和Connection#createChannel返回的 连接将 实现com.rabbitmq.client.Recoverable,从而提供两个具有相当描述性名称的方法:
请注意,您目前需要将连接和频道强制转换为可恢复 ,以便使用这些方法。
连接断开时 使用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秒。这种设计假设即使很多网络故障是短暂的并且通常是短暂的,它们也不会瞬间消失。延迟还避免了服务器端资源清理(例如独占或自动删除队列删除)与在相同资源上新打开的连接上执行的操作之间的固有竞争条件。
默认情况下,连接恢复尝试将以相同的时间间隔继续,直到成功打开新连接。通过向ConnectionFactory#setRecoveryDelayHandler提供RecoveryDelayHandler 实现实例,可以使恢复延迟动态化。使用动态计算的延迟间隔的实现应避免使用太低的值(根据经验,低于2秒)。
当连接处于恢复状态时,任何在其频道上尝试的发布都将被拒绝并发生异常。客户端当前不对此类传出消息执行任何内部缓冲。应用程序开发人员有责任跟踪此类消息,并在恢复成功时重新发布这些消息。发布者确认是一种协议扩展,应由不能承受邮件丢失的发布者使用。
由于频道级异常,当频道关闭时,连接恢复不会启动。此类异常通常表示应用程序级别的问题。该库无法做出明智的决定。
即使在连接恢复启动后,也不会恢复关闭的频道。这包括明确关闭的频道和上面的频道级异常情况。
使用手动确认时,与RabbitMQ节点的网络连接可能在消息传递和确认之间失败。连接恢复后,RabbitMQ将重置所有频道上的交付标签。这意味着 具有旧传递标记的basic.ack,basic.nack和basic.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启用指标收集 :
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启用指标收集 :
JmxMeterRegistry registry = new JmxMeterRegistry();
MicrometerMetricsCollector metrics = new MicrometerMetricsCollector(registry);
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setMetricsCollector(metrics);
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(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#exchangeDelete,Channel#queueUnbind和Channel#exchangeUnbind 在RabbitMQ 3.3.x中是幂等的(删除那些不会导致异常的内容)来实现。
为了方便编程,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加密客户端和代理之间的通信 。还支持客户端和服务器身份验证(也称为对等验证)。以下是使用Java客户端加密的最简单方法:
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5671);
factory.useSslProtocol();
请注意,客户端不会在上面的示例中强制执行任何服务器身份验证(对等证书链验证)作为默认值,使用“信任所有证书” TrustManager。这对于本地开发很方便,但容易发生中间人攻击,因此不建议用于生产。要了解有关RabbitMQ中TLS支持的更多信息,请参阅TLS指南。如果您只想配置Java客户端(尤其是对等验证和信任管理器部分),请阅读TLS指南的相应部分。
如果您对本指南的内容或与RabbitMQ相关的任何其他主题有疑问,请不要犹豫,在RabbitMQ邮件列表中询问他们。
如果您想对网站做出改进,可以在GitHub上找到它的来源。只需分叉存储库并提交拉取请求。谢谢!