RabbitMQ-CookBook-第2章-超越AMQP标准
本章我们将覆盖:
- 如何使用消息过期
- 如何使指定队列上的消息过期
- 如何让队列过期
- 管理驳回的(rejected)或过期的消息
- 理解其它备用交换器扩展
- 理解有效user-ID扩展
- 通知队列消息者失败
- 理解交换器到交换器扩展
- 在消息中嵌入消息目的地
介绍
在本章中,我们将展示关于RabbitMQ扩展上的一些食谱.这些扩展不是AMQP 0-9-1标准的一部分,使用它们会破坏其它AMQPbroker的兼容性。
另一方面, 在AMQP 0-10 (http://www.amqp.org/specification/0-10/amqp-org-download)中也出现了轻微的变化,这是一个简单通往那里的路径.最后, 它们通常是优化问题的有效解决方案。
另一方面, 在AMQP 0-10 (http://www.amqp.org/specification/0-10/amqp-org-download)中也出现了轻微的变化,这是一个简单通往那里的路径.最后, 它们通常是优化问题的有效解决方案。
本章中的例子将更为真实,例如,配置参数,如列表和交换器, 以及路由键名称将定义在
Constants接口中。事实上,一个真正的应用程序会遵循这样的准则从配置文件中读取配置文件,以在不同应用程序中共享。
然而,在下面的例子中,为了更简短和较好的可读性,我们并没有指定
Constants的命名空间。
如何让消息过期
在本食谱中,我们将展示如何让消息过期.食谱的资源可在
Chapter02/Recipe01/Java/src/rmqexample中找到,如:
- Producer.java
- Consumer.java
- GetOne.java
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
本示例的核心是Producer.java文件.为了产生在给定生存时间(TTL)后过期的消息,我们需要执行下面的步骤:
1. 创建或声明一个用来发送消息的交换器, 并将其绑定到队列上,就像第1章使用AMQP看到的一样:
channel.exchangeDeclare(exchange, "direct", false);
channel.queueDeclare(queue, false, false, false, null);
channel.queueBind(queue, exchange, routingKey);
2. 像下面这样初始化可选消息属性TTL:
BasicPropertiesmsgProperties = new
BasicProperties.Builder().expiration("20000").build();
3. 使用下面的代码来发布消息:
channel.basicPublish(exchange, routingKey, msgProperties,
statMsg.getBytes());
如何工作
在这个例子中,生产者创建了一个交换器,一个命名队列,并将它们进行了绑定,当队列上没有附着任何消费者,过期消息就显得非常有意义了。
设置过期时间TTL (以毫秒设置),会促使RabbitMQ在消息过期时,如果消息没有被客户端及时消费,立即删除消息
.
在我们的例子中,我们假设应用程序发布了JVM资源统计信息到给定队列,如果存在消费者,那么会像平时一样,获取到实时数据,反之,如果不存在这样的消费者,那么消息会给定生存时间后立即过期
。通过这种方式,可以避免我们收集大量的数据。一旦消息者绑定到了队列中,它会得到先前的消息(未过期)。进一步的试验,你可以用
GetOne.java文件来替换Consumer.java文件运行.
在调用 channel.basicGet() 时,会使你一次只能消费一个消息。
TIP
可使用channel.basicGet()方法来检查未消费消息的队列
.也可以通过为第二参数传递false来调用,即
autoAck标志.
在这里我们可以通过调用
rabbitmqctl list_queues来监控RabbitMQ队列的状态。
也可参考
默认情况下,过期消息会丢失,但它们可以路由到其它地方。可参考管理拒绝消息或过期消息食谱来了解更多信息
.
如何让指定队列上的消息过期
在本食谱中,我们将展示指定消息TTL的第二种方式.这次,我们不再通过消息属性来指定,而是通过缓存消息的队列来进行指定。在这种情况下,生产者只是简单地发布消息到交换器中,因此,在交换器上绑定标准队列和过期消息队列是可行的。
要在这方面进行备注,须存在一个创建自定义的队列的消费者。生产者是相当标准的
.
像前面的食谱一样,你可以在Chapter02/Recipe02/Java/
src/rmqexample找到这三个源码。
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
现在我们将展示创建特定消息TTL队列的必要步骤。在我们的例子中,需要在
Consumer.java文件中执行下面的步骤:
1. 按下面来声明交换器:
channel.exchangeDeclare(exchange, "direct", false);
2. 创建或声明队列,像下在这样为x-message-ttl可选参数指定10,000毫秒的超时时间:
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-message-ttl", 10000);
channel.queueDeclare(queue, false, false, false, arguments);
3. 绑定队列到交换器上:
channel.queueBind(queue, exchange, routingKey);
如何工作
在这个例子中,为了最终分析,我们再次假设生产者发送了JVM统计数据给RabbitMQ。最终因为
Producer.java文件将其发到一个交换机,如果无消费者连接的话,消息最终会丢失。
想要监控或分析这些统计数据的消费有下面三种选择:
- 绑定到一个临时队列,即调用无参的channel.queueDeclare()方法
- 绑定到一个非自动删除的命名队列
- 绑定到一个非自动删除的命名队列,并且指定x-message-ttl ,如步骤2中展示的一样.
在第一种情况中,消费者将获取实时统计数据,但当它掉线期间,它将不能在数据上执行分析。
在第二种情况中,为了能让它掉线期间,能获取到发送的消息,可以使用一个命名队列(最终是持久化的
).但在掉线较长时间后,再重启时,它将有巨大的backlog来进行恢复,因此在队列中可能存在大部分旧消息的垃圾。
在第三种情况中,旧消息垃圾会通过RabbitMQ自己来执行,以使我们从消费者和broker中获益。
更多
当设置per-queue TTL, 就像本食谱中看到的一样,只要未到超时时间,消息就不会被丢弃,此时消费者还可以尝试消费它们。
当使用queue TTL时, 这里有一个细微的变化,但使用per-message TTL时,在broker队列中可能会存在过期消息
.
在这种情况下,这些过期消息仍然会占据资源(内存),同时broker统计数据中仍然会计数,直到它们不会到队列头部时。
也中参考
在这种情况下,过期消息也会恢复。参考管理驳回或过期消息食谱
.
如何让队列过期
在第三种情况中,TTL不关联任何消息,只关联对列。这种情况对于服务器重启和更新,是一个完美的选择。一旦TTL超时,
在最后一个消费者停止消费后,RabbitMQ会丢弃队列.
前面TTL相关食谱,你可在
Chapter02/Recipe03/Java/src/rmqexample 中找到 Producer.java , Consumer.java ,and GetOne.java 相关文件。
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
在前面的例子中,扩展只需要关注Consumer.java :
1. 使用下面的代码来创建或声明交换器:
channel.exchangeDeclare(exchange, "direct", false);
2. 创建或声明队列,并为x-expires可选参数指定30,000毫秒的超时时间:
Map<String, Object> arguments = new HashMap<String,
Object>();
arguments.put("x-expires", 30000);
channel.queueDeclare(queue, false, false, false,
arguments);
3. 将队列绑定到交换器上:
channel.queueBind(queue, exchange, routingKey);
如何工作
当我们运行Consumer.java或 GetOne.java 文件的时候, 超时队列已经创建好了,在消费者附着到队列上或调用
channel.basicGet()时,它将持续存在
.
只有当我们停止这两个操作超过30秒时,队列才会被删除,并且队列包含的消息也会清除。
TIP
无论生产者是否向其发送了消息,队列事实上都是独立删除的。
在这个试验课程中,我们可通过 rabbitmqctl list_queues 命令来监控RabbitMQ 队列状态
.
因此,我们可以想像一种场景,有一个统计分析程序需要重启来更新其代码。由于命名队列有较长的超时时间,因此重启时,不会丢失任何消息。如果我们停止,队列会在超过TTL后被删除,无价值的消息将不再存储。
管理驳回或过期消息
在这个例子中,我们将展示如何使用死信交换器来管理过期或驳回的消息
. 死信交换器是一种正常的交换器,死消息会在这里重定向,如果没有指定,死消息会被broker丢弃。
你可以在Chapter02/Recipe04/Java/src/rmqexample中找到源码文件
:
- Producer.java
- Consumer.java
要尝试过期消息,你可以使用第一个代码来发送带TTL的消息,就如如何使指定队列上消息过期食谱中描述的一样
.
一旦启动了,消费者不允许消息过期,但可以可以驳回消息,最终导致成为死消息。
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
下面的步骤展示了使用死信交换器来管理过期或驳回消息:
1. 创建一个工作交换品节和死信交换器:
channel.exchangeDeclare(Constants.exchange, "direct", false);
channel.exchangeDeclare(Constants.exchange_dead_letter,
"direct", false);
2. 创建使用使用死信交换器和 x-message-ttle参数的队列
:
arguments.put("x-message-ttl", 10000);
arguments.put("x-dead-letter-
exchange",exchange_dead_letter);
channel.queueDeclare(queue, false, false, false,
arguments);
3. 然后像下面这样绑定队列:
channel.queueBind(queue, exchange, "");
4. 最后使用channel.basicPublish()来向交换器发送消息 .
5. 要尝试驳回消息,我们需要配置一个消费者,就像前面例子中看到的一样,并使用下面的代码来驳回消息:
basicReject(envelope.getDeliveryTag(), false);
如何工作
我们先从第一个场景开始(单独使用producer): the expired messages. 在步骤中,我们创建两个交换器,工作交换器和死信交换器。在步骤2中,我们使用下面两个可选参数来创建队列:
- 使用arguments.put("x-message-ttl", 10000)来设置消息TTL ,正如如何使指定队列上消息过期食谱中描述的一样.
- 使用arguments.put("x-dead-letter-exchange", exchange_dead_letter)来设置死信交换器名称;
正如你所看到的,我们只是在配置中添加了可选的队列参数。因此,当生产者发送消息到交换器时,它会队列参数来路由。消息会在10秒后过期,之后它会重定向到
exchange_dead_
letter
TIP
死信交换器是一个标准的交换器,因此你可以基于任何目的来使用
.
对于第二种场景,食谱的消费者会驳回消息.当消费者得到消息后, 它会使用basicReject()方法来发回一个否定应答(nack),当broker收到nack时,它会将消息重定向到
exchange_dead_letter. 通过在死信交换器上绑定队列,你可以管理这些消息。
当消息重定向到死信队列时,broker会修改header消息,并在x-dead键中增加下面的值:
- reason : 表示队列是否过期的或驳回的(requeue =false )
- queue : 表示队列源,例如stat_queue_02/05
- time : 表示消息变为死信的日期和时间
- exchange : 表示交换器,如monitor_exchange_02/05
- routing-keys : 表示发送消息时原先使用的路由键
要在实践中查看这些值,你可使用GetOneDeadLetterQ 类.这可以创建
queue_dead_letter队列并会绑定到exchange_dead_letter
更多
你也可以使用arguments.put("x-dead-letter-routing-key", "myroutingkey")来指定死信路由键
,它将会代替原来的路由键.这也就意味着你可以用不同的路由键来将不同消息路由到同一个队列中。相当棒。
理解交替交换器扩展
目前,在第1章使用 AMQP中我们已经展示了如何来处理未路由消息(消息发布到了交换器,但未能达到队列)
. AMQP让生产者通过此条件进行应答,并最终决定是否有需要再次将消息分发到不同的目的地。通过这种扩展,我们可在broker中指定一个交替交换器来路由消息
,而并不会对生产者造成更多的干预,本食谱的代码在
Chapter02/Recipe05/Java/src/rmqexample .
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
在本食谱中,我们会在Producer.java中声明交替交换器.
1. 将交换器的名字(无目的地路由消息)-
alternateExchange ,放到可选参数map的"alternate-
exchange"中,如下所示:
Map<String, Object> arguments = new HashMap<String,
Object>();
arguments.put("alternate-exchange", alternateExchange);
2. 通过传递arguments map来声明交换器来发送消息:
channel.exchangeDeclare(exchange, "direct", false, false,
arguments);
3. 声明alternateExchange自身(已经在步骤1中指定了),如下所示:
channel.exchangeDeclare(alternateExchange, "direct",
false);
4. 声明标准化持久化队列,并使用路由键alertRK将其绑定到alternateExchange交换器中
:
channel.queueDeclare(missingAlertQueue, true, false, false,
null);
channel.queueBind(missingAlertQueue, alternateExchange,
alertRK);
如何工作
在这个例子中,我们再次使用了生成统计数据的producer,正如先前的例子一样
.但这次,我们添加了路由键来让producer指定一个重要的级别,名为
infoRK或alertRK (在例子中是随机分配的).如果你运行一个
producer以及至少一个consumer,将不会丢失任何消息,并且一切都会正常工作
.
TIP
Consumers在交换器和队列的声明中,必须传递相同的可选参数,否则会抛出异常。
但如果没有消费者监听的话,而我们不想丢失报警的话,这就是为什么必须选择让
producer创建alternateExchange (步骤3)并将其绑定到持久队列-
missingAlertQueue的原因 (步骤4).
在单独运行producer的时候,你将看到报警存储在这里.alternate交换器让我们在不丢失消息的情况下可以路由消息
.你可通过调用r
abbitmqctllist_queues或运行CheckAlerts.java来检查状态 .
最后的代码让我们可以查看队列的内容和第一个消息,但不会进行消费。完成这种行为是简单的,它足可以避免这种事实:RabbitMQ client发送了ack,消息未消费,而只是进行监控。
现在,如果我们再次运行Consumer.java文件,它会从m
issingAlertQueue队列中获取并消费消息.这不是自动的,我们可以选择性地从此队列中获取消息。
通过创建第二个消费者实例
( missingAlertConsumer ) 并使用相同的代码来从两个不同队列消费消息就可以完成这种效果。如果在处理实时消息时,想要得到不同的行为,那么我们可以创建一个不同的消费者。
更多
在这个例子中,步骤3和步骤4是可选的。 当定义交换器时,可为交替交换器指定名称,对于其是否存在或是否绑定到任何队列上,并不作强制要求 。如果交替交换器不存在,生产者可通过在丢失消息上设置mandatory标志来得到应答,就如在第1章中处理未路由消息食谱中看到的一样。
甚至有可能出现另一种交换器-它自己的备用交换器,备用交换器可以是链式的,并且无目的地消息在按序地重试,直到找到一个目的地。
如果在交换器链的末尾仍然没有找到目的地,消息将会丢失,生产者可通过调设置
mandatory 标志和指定一个合适的
ReturnListener参数得到通知。
理解经过验证的user-ID扩展
依据AMQP, 当消费者得到消息时,它是不知道发送者信息的。一般说来,消费者不应该关心是谁生产的消息,对于生产者-消费者解藕来说是相当有利的。然而,有时出于认证需要,为了达到此目的,
RabbitMQ 提供了有效的user-ID扩展。
在本例中,我们使用有效user-IDs模拟了订单。你可在
Chapter02/Recipe06/Java/src/rmqexample中找到源码.
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
完成下面的步骤,以使用经过验证的user IDs来模拟订单:
1. 像下面一样声明或使用持久化队列:
channel.queueDeclare(queue, true, false, false, null);
2.发送消息时,使用BasicProperties对象,在消息头中指定一个user ID
:
BasicProperties messageProperties = new
BasicProperties.Builder()
.timestamp(new Date())
.userId("guest");
channel.basicPublish("",queue, messageProperties,
bookOrderMsg.getBytes());
3. 消费者获取到订单后,可像下面这样打印订单数据和消息头:
System.out.println("The message has been placed by
"+properties.getUserId());
如何工作
当设置了user-ID时,RabbitMQ 会检查是否是同一个用户打开的连接。在这个例子中,用户是
guest ,即RabbitMQ默认用户.
通过调用properties.getUserId() 方法,消费者可以访问发送者user ID。如果你想在步骤2中设置非当前用户的userId,
channel.basicPublish()会抛出异常.
TIP
如果不使用user-ID属性,用户将是非验证的,
properties.getUserId()方法会返回null.
也可参考
要更好的理解这个例子,你应该知道用户和虚拟机管理,这部分内容将在下个章节中讲解。在下个章节中,我们将了解如何通过在应用程序中使用SSL来提高程序的安全性。只使用
user-ID属性,我们可保证用户已认证,但所有信息都是未加密的,因此很容易暴露。
队列失败时通知消费者
根据AMQP标准,消费者不会得到队列删除的通知。一个正在删除队列上等待消息的消费者不会收到任何错误信息,并会无限期地等待。然而,R
abbitMQ client提供了一种扩展来让消息收到一个cancel参数-即消费者cancel通知。我们马上就会看到这个例子,你可在
Chapter02/Recipe07/Java/src/rmqexample 中找到代码.
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样。
如何做
为了能让扩展工作,你只需要执行下面的步骤:
1.在自定义的消费者中覆盖handleCancel()方法,可继承于
com.rabbitmq.client.DefaultConsumer (指的是
ActualConsumer.java ):
public void handleCancel(String consumerTag) throws
IOException {
...
}
如何工作
在我们的例子中,我们选择实现一个消费者,这个消费者只在生产者是持久化的,且队列是由生产者创建的情况下才能工作。
因此,如果队列是非持久化的,Consumer.java文件会立即错误退出. 此行为可以通过调用
channel.queueDeclarePassive()来完成 .
Producer.java类在其启动时会创建队列,并在其关闭时调用channel.queueDelete()方法删除队列,如果当队列关闭时,而消费者正在消费队列,那么RabbitMQ client会调用步骤1中覆盖的handleCancel()方法来立即通知消费者。
相对于显示调用channel.basicCancel() 消费者使用handleCancel()方法可以任意理由来退出。只有在这种情况下,
RabbitMQ
client library会调用Consumer接口的方法: handleCancelOK()
更多
消费者cancel通知是client library的扩展,而不是
AMQP client libraries的常规方法.一个实例它们的library必须将其声明为可选属性
(参考 http://www.rabbitmq.com/consumer-cancel.
html#capabilities ).
RabbitMQ client library 支持并声明了这种特性。
也可参考
在集群中,如果一个节点失效了,也会发生同样的事情
:client在队列删除后仍然得不到通知,除非它定义了覆盖了自己的
handleCancel()方法。关于这点的更多信息,可参考
Chapter 6,开发可伸缩性应用程序。
理解交换器到交换器扩展
默认情况下,AMQP支持交换器到队列,但不支持交换器到交换器绑定。在本例中,我们将展示如何使用
RabbitMQ 交换机到交换机扩展.
在本例中,我们将合并来自两个不同交换器的消息到第三个交换器中
.你可以在Chapter02/Recipe08/Java/src/rmqexample找到源码.
准备
为了使用本食谱,我们需要设置Java开发环境,如第1章节(使用AMQP)介绍章节中说明的一样,并像广播消息食谱中来运行生产者以及使用topic交换器来处理消息路由。
如何做
完成下面的步骤来使用RabbitMQ 交换器到交换器扩展:
1. 使用下面的代码来声明我们需要追踪消息的交换器:
channel.exchangeDeclare(exchange, "topic", false);
2. 使用
exchangeBind()来绑定其它例子中的交换器 :
channel.exchangeBind(exchange,ref_exchange_c1_8,"#");
channel.exchangeBind(exchange,ref_exchange_c1_6,"#");
3. 启动追踪消费者:
TraceConsumer consumer = new TraceConsumer(channel);
String consumerTag = channel.basicConsume(myqueue, false,
consumer);
如何工作
在步骤1中,我们创建了一个新的交换器,在步骤2中我们绑定到了下面的交换器:
- ref_exchange_c1_6 (广播消息) 与exchange绑定.
- ref_exchange_c1_8 (使用topic来处理消息路由)与exchange绑定 .
在步骤3中, 消费者可以绑定一个队列到exchange上以任意地获取所有消息
.
交换器到交换器扩展的工作方式与交换器到队列绑定过程类似,你也可以指定一个路由键来过滤消息.在步骤2中,我们可以使用#(匹配所有消息)来作为路由键。通过改变路由键你可以使用制作一个
filter!
在消息中内嵌消息目的地
在本例子中,我们会展示如何发送单个发布带路由键的的消息
.标准AMQP不提供此特性,但幸运的是,RabbitMQ使用消息属性header提供了此特性
. 这种扩展称为sender-selected分发.
此扩展的行为类似于电子邮件逻辑.它使用Carbon Copy (CC)和Blind
Carbon Copy (BCC).这也是为什么能在 Chapter02/Recipe09/Java/src/rmqexample中找到CC和BCC consumers的理由:
- Producer.java
- Consumer.java
- StatsConsumer.java
- CCStatsConsumer.java
- BCCStatsConsumer.java
准备
To use this recipe, we need to set up the Java development environment as indicated in the
Introduction section of Chapter 1, Working with AMQP.
如何做
完成下面的步骤来使用单个发布带路由键的的消息
:
1. 使用下面的代码来创建或声明交换器:
channel.exchangeDeclare(exchange, "direct", false);
2. 在消息的header属性中指定CC , BCC路由键
:
List<String> ccList = new ArrayList<String>();
ccList.add(backup_alert_routing_key);
headerMap.put("CC", ccList);
List<String> ccList = new ArrayList<String>();
bccList.add(send_alert_routing_key);
headerMap.put("BCC", bccList);
BasicProperties messageProperties = new BasicProperties.Builder().
headers(headerMap).
build();
channel.basicPublish(exchange, alert_routing_key,
messageProperties, statMsg.getBytes());
3. 使用下面的三个路由键来绑定三个队列three queues to the exchange using the following three routing keys:
channel.queueBind(myqueue,exchange, alert_routing_key);
channel.queueBind(myqueueCC_BK,exchange,
backup_alert_routing_key);
channel.queueBind(myqueueBCC_SA,exchange,
send_alert_routing_key);
4. 使用三个消费者来消费消息
如何工作
当生产者使用CC和BCC消息属性来发送消息时,broker会在所有路由键的队列上拷贝消息 。在本例中,stat类会直接使用路由键alert_routing_key来向交换器发送消息,同时它也会将消息拷贝到使用CC和BCC参数信息来将消息拷贝到
myqueueCC_BK,myqueueBCC_SA队列中。
当像e-mails一样发生时,在分发消息到队列前,BCC信息会被broker从消息头中删除,你可查看所有我们示例消费者的输出来观察这种行为。
更多
正常情况下,AMQP不会改变消息头,但BCC扩展是例外。这种扩展可减少发往broker的消息数目。没有此扩展,生产者只能使用不同的路由键来发送多个消息的拷贝。