mandatory和immediate是channel .basicPublish方法中的两个参数,它们都有 当消息传递过程中不可达目的地时将消息返回给生产者的功能。RabbitMQ提供的备份交换器 (Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存 储起来,而不用返回给客户端。
当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件 的队列,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参 数设置为false时,出现上述情形,则消息直接被丢弃。
通过调用 channel. addReturnListener 来添加 ReturnListener 监听器实现。
channel.basicPublish(EXCHANGENAME,true,MessageProperties.PERSISTENT_TEXT_PLAIN,"mandatorytest".getBytes());
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
String message = new String(body);
System, out .println ("Basic.Return 返回的结果是:"+message);
}
});
上面代码中生产者没有成功地将消息路由到队列,此时RabbitMQ会通过Basic.Return 返回“mandatory test”这条消息,之后生产者客户端通过ReturnListener监听到了这个事 件,上面代码的最后输出应该是“Basic.Return返回的结果是:mandatory test”。
当immediate参数设为true时,如果交换器在将消息路由到队列时发现队列上并不存在 任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时, 该消息会通过Basic .Return返回至生产者。
概括来说,mandatory参数告诉服务器至少将该消息路由到一个队列中,否则将消息返 回给生产者。immediate参数告诉服务器,如果该消息关联的队列上有消费者,则立刻投递; 如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用将消息存入队列而等待消费者了
RabbitMQ 3.0版本开始去掉了对immediate参数的支持,对此RabbitMQ官方解释是: immediate参数会影响镜像队列的性能,增加了代码复杂性,建议采用TTL和DLX的方法替代
备份交换器,英文名称为Alternate Exchange,简称AE,或者更直白地称之为“备胎交换器”。 生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失; 如果设置了 mandatory参数,那么需要添加ReturnListener的编程逻辑,生产者的代码将 变得复杂。如果既不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器, 可以将未被路由的消息存储在RabbitMQ中,再在需要的时候去处理这些消息。
可以通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加 alternate-exchange参数来实现,也可以通过策略的方式实现。 如果两者同时使用,则前者的优先级更高,会覆盖掉Policy的设置。
Map args = new HashMap
上面的代码中声明了两个交换器normaffixchange和myAe,分别绑定了 normalQueue和 unroutedQueue这两个队列,同时将myAe设置为normalExchange的备份交换器。注意myAe 的交换器类型为fanout。
如果此时发送一条消息到normalExchange上,当路由键等于“normalKey”的 时候,消息能正确路由到normalQueue这个队列中。如果路由键设为其他值,比如“errorKey”, 即消息不能被正确地路由到与normalExchange绑定的任何队列上,此时就会发送给myAe,进 而发送到unroutedQueue这个队列。
备份交换器其实和普通的交换器没有太大的区别,为了方便使用,建议设置为fanout类型, 如若想设置为direct或者topic的类型也没有什么不妥。需要注意的是,消息被重新发送到备份交换器时的路由键和从生产者发出的路由键是一样的。
考虑这样一种情况,如果备份交换器的类型是direct,并且有一个与其绑定的队列,假设绑 定的路由键是keyl,当某条携带路由键为key2的消息被转发到这个备份交换器的时候,备份交换器没有匹配到合适的队列,则消息丢失。如果消息携带的路由键为keyl,则可以存储到队 列中。
对于备份交换器,总结了以下几种特殊情况:
如果设置的备份交换器不存在,客户端和RabbitMQ服务端都不会有异常出现,此时消 息会丢失。
如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都不会有异常出现,此 时消息会丢失。
如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都不会有异常出现, 此时消息会丢失。
如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。
TTL,Time to Live的简称,即过期时间。RabbitMQ可以对消息和队列设置TTL。
目前有两种方法可以设置消息的TTL。
第一种方法是通过队列属性设置,队列中所有消息 都有相同的过期时间。
第二种方法是对消息本身进行单独设置,每条消息的TTL可以不同。
如 果两种方法一起使用,则消息的TTL以两者之间较小的那个数值为准。消息在队列中的生存时 间一旦超过设置的TTL值时,就会变成“死信”(Dead Message),消费者将无法再收到该消息 (但这点不是绝对的)
针对每条消息设置TTL的方法是在channel .basicPublish方法中加入expiration 的属性参数,单位为毫秒。
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode (2) ;//持久化消息
builder .expiration ("60000") ;//设置 TTL=60000ms;
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName,routingKey,mandatory,properties,"ttlTestMessage".getBytes());
或者
AMQP.BasicProperties properties = new AMQP.BasicProperties();
Properties.setDeliveryMode(2);
properties.setExpiration("60000");
channel.basicPublish(exchangeName,routingKey,mandatory,properties,"ttlTestMessage".getBytes());
对于第一种设置队列TTL属性的方法,一旦消息过期,就会从队列中抹去,而在第二种方 法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费 者之前判定的。
为什么这两种方法处理的方式不一样?因为第一种方法里,队列中已过期的消息肯定在队 列头部,RabbitMQ只要定期从队头开始扫描是否有过期的消息即可。而第二种方法里,每条消 息的过期时间不同,如果要删除所有过期消息势必要扫描整个队列,所以不如等到此消息即将 被消费时再判定是否过期,如果过期再进行删除即可。
通过channel .queueDeclare方法中的x-expires参数可以控制队列被自动删除前处于未使用状态的时间。
未使用状态:未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未调用过Basic.Get命令。
设置队列里的TTL可以应用于类似RPC方式的回复队列,在RPC中,许多队列会被创建 出来,但是却是未被使用的。
RabbitMQ会确保在过期时间到达后将队列删除,但是不保障删除的动作有多及时。在 RabbitMQ重启后,持久化的队列的过期时间会被重新计算。
用于表不过期时间的x-expires参数以毫秒为单位,并且服从和x-message-ttl —样 的约束条件,不过不能设置为0。比如该参数设置为1000,则表示该队列如果在1秒钟之内未 使用则会被删除。
通过队列属性设置消息TTL的方法是在channel.queueDeclare方法中加入 x_message-ttl参数实现的,这个参数的单位是毫秒。
Map argss = new HashMap();
argss.put("x-message-ttl",6000);
channel.queueDeclare(queueName, durable, exclusive, autoDelete, argss);
如果不设置TTL,则表示此消息不会过期;如果将TTL设置为0,则表示除非此时可以直 接将消息投递到消费者,否则该消息会被立即丢弃,这个特性可以部分替代RabbitMQ 3.0版本 之前的immediate参数,之所以部分代替,是因为immediate参数在投递失败时会用 Basic.Return将消息返回
DLX,全称为Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个 交换器就是DLX,绑定DLX的队列就称之为死信队列。
消息变成死信一般是由于以下几种情况:
今消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;
今消息过期;
今队列达到最大长度。
DLX也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实 际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ就会自动地将这个消息重 新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消 息以进行相应的处理,这个特性与将消息的TTL设置为0配合使用可以弥补immediate参数 的功能。
channel.exchangeDeclare("exchange.dlx", "direct", true);
channel.exchangeDeclare("exchange.normal", "fanout", true);
Map args = new HashMap();
args .put (x-message-ttl", 10000);
args.put("x-dead-letter-exchange", "exchangedlx");
args.put("x-dead-letter-routing-key", "routingkey");
channel.queueDeclare("queue.normal", true, false, false, args);
channel.queueBind("queue.normal", "exchange.normal", ”");
channel.queueDeclare("queue.dlx", true, false, false, null);
channel.queueBind("queue.dlx", "exchange.dlx", "routingkey");
channel.basicPublish("exchange.normal","rk",MessageProperties.PERSISTENT_TEXT_PLAIN,"dlx".getBytes());
生产者首先发送一条携带路由键为“rk”的消息,然后经过交换器
exchange.normal顺利地存储到队列queue.normal中。由于队列queue.normal设置了过期时间为 10s,在这10s内没有消费者消费这条消息,那么判定这条消息为过期。由于设置了 DLX,过期 之时,消息被丢给交换器exchange.dlx中,这时找到与exchange.dlx匹配的队列queue.dlx,最 后消息被存储在queue.dlx这个死信队列中。
对于RabbitMQ来说,DLX是一个非常有用的特性。它可以处理异常情况下,消息不能够 被消费者正确消费(消费者调用了 Basic.Nack或者Basic.Reject)而被置入死信队列中 的情况,后续分析程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进 而可以改善和优化系统。DLX配合TTL使用还可以实现延迟队列的功能,
延迟队列存储的对象是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不 想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
延迟队列的使用场景有很多,比如:
今在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内 没有支付成功,那么这个订单将进行异常处理,这时就可以使用延迟队列来处理这些 订单了。
今用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将 用户指令发送到延迟队列,当指令设定的时间到了再将指令推送到智能设备。
在AMQP协议中,或者RabbitMQ本身没有直接支持延迟队列的功能,但是可以通过前面 所介绍的DLX和TTL模拟出延迟队列的功能。
在下图中,不仅展示的是死信队列的用法,也是延迟队列的用法,对于queue.dlx这个死 信队列来说,同样可以看作延迟队列。假设一个应用中需要将每条消息都设置为10秒的延迟, 生产者通过exchange.normal这个交换器将发送的消息存储在queue.normal这个队列中。消费者 订阅的并非是queue.normal这个队列,而是queue.dlx这个队列。当消息从queue.normal这个队 列中过期之后被存入queue.dlx这个队列中,消费者就恰巧消费到了延迟10秒的这条消息。
优先级队列,顾名思义,具有高优先级的队列具有高的优先权,优先级高的消息具备优先被消费的特权。
Map args = new HashMap();
args.put("x-max-priority", 10);
channel.queueDeclare("queue.priority", true, false, false, args);
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.priority(5);
AMQP.BasicProperties properties = builder.build();
channel.basicPublish("exchange_priority","rk_priority",properties,("messages ").getBytes());
上面的代码中设置消息的优先级为5。默认最低为0,最高为队列设置的最大优先级。优先 级高的消息可以被优先消费,这个也是有前提的:如果在消费者的消费速度大于生产者的速度 且Broker中没有消息堆积的情况下,对发送的消息设置优先级也就没有什么实际意义。因为生 产者刚发送完一条消息就被消费者消费了,那么就相当于Broker中至多只有一条消息,对于单 条消息来说优先级是没有什么意义的。
RPC,是Remote Procedure Call的简称,即远程过程调用。它是一种通过网络从远程计算 机上请求服务,而不需要了解底层网络的技术。RPC的主要功用是让构建分布式计算更容易, 在提供强大的远程调用能力时不损失本地调用的语义简洁性。
通俗点来说,假设有两台服务器A和B, —个应用部署在A服务器上,想要调用B服务器 上应用提供的函数或者方法,由于不在同一个内存空间,不能直接调用,需要通过网络来表达 调用的语义和传达调用的数据。
RPC的协议有很多,比如最早的CORBA、Java RMI、WebService的RPC风格、Hessian、 Thrift 甚至还有 Restfal API。
一般在RabbitMQ中进行RPC是很简单。客户端发送请求消息,服务端回复响应的消息。 为了接收响应的消息,我们需要在请求消息中发送一个回调队列(参考下面代码中的replyTo)。
String 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...
BasicProperties这个类包含14个属性,这里就用到两个属性:
replyTo:通常用来设置一个回调队列。
correlationld:用来关联请求(request)和其调用RPC之后的回复(response)。
如果像上面的代码中一样,为每个RPC请求创建一个回调队列,则是非常低效的。但是幸 运的是这里有一个通用的解决方案一可以为每个客户端创建一个单一的回调队列。
这样就产生了一个新的问题,对于回调队列而言,在其接收到一条回复的消息之后,它并 不知道这条消息应该和哪一个请求匹配。这里就用到correlationld这个属性了,我们应该 为每一个请求设置一个唯一的correlationld。之后在回调队列接收到回复的消息时,可以 根据这个属性匹配到相应的请求。如果回调队列接收到一条未知correlationld的回复消息, 可以简单地将其丢弃。
如果不是丢弃只是看错失败的话,那么可能出现重复消费的问题,这个时候需要保证幂等性。
1.当客户端启动时,创建一个匿名的回调队列(名称由RabbitMQ自动创建,图4-7中 的回调队列为 amq.gen-LhQz 1 gv3GhDOv8PIDabOXA)。
2.客户端为RPC请求设置2个属性:replyTo用来告知RPC服务端回复请求时的目的 队列,即回调队列;correlationld用来标记一个请求。
3.请求被发送到rpc_queue队列中。
4.RPC服务端监听rpC_qUeUe队列中的请求,当请求到来时,服务端会处理并且把带有 结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
5.客户端监听回调队列,当有消息时,检查correlationld属性,如果与请求匹配, 那就是结果了。
服务端代码:
public class RPCServer {
private static final String RPC_QUEUE一NAME = "rpc_queue";
public static void main(String args⑴ throws Exception {
//省略了创建Connection和Channel的过程,具体可以参考1.4.4节
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
System.out.println(" [x] Awaiting RPC requests");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,AMQP.BasicProperties properties, byte[] body) throws IOException {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties .Builder().correlationld(properties.getCorrelationld()).build();
String response = "";
try {
String message = new String(body, "UTF-8"); int n = Integer.parselnt(message);
System.out.println(" [•] fib (" + message + ")"); response += f ib(n);
} catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
channel.basicPublish("", properties.getReplyTo(), replyProps,
response.getBytes("UTF-8M)); channel.basicAck(envelope.getDeliveryTag(), false);
channel•basicConsume(RPC—QUEUE_NAME, false, consumer);
}
}
}
}
private static int fib(int n){
if (n == 0) return 0;
if (n == 1} return 1;
return fib(n - 1) + fib(n - 2);
}
}
客户端代码:
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
private QueueingConsumer consumer;
public RPCClient() throws IOException, TimeoutException {
//省略了创建Connection和Channel的过程,具体可以参考1.4.4节
replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true,consumer);
public String call(String message) throws IOException,ShutdownSignalException, ConsumerCancelledException, InterruptedException {
String response = null;
String corrld = UUID.randomUUID() .toString ();
BasicProperties props = new BasicProperties.Builder().correlationld(corrld).replyTo(replyQueueName).build();
channel.basicPublishrequestQueueName, props, message.getBytes());
while(true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
if(delivery.getProperties().getCorrelationld().equals(corrld)){
response = new String (delivery .getBody);
break;
}
}
return response;
}
public void close() throws Exception{
connection.close();
}
public static void main(String args[]) throws Exception{
RPCClient fibRpc = new RPCClientO;
System.out.println(" [x] Requesting fib(30)");
String response = fibRpc.call("30");
System.out.println(" [.] Got ,'"+response+"'");
fibRpc.close();
}
持久化可以提髙RabbitMQ的可靠性,以 防在异常情况(重启、关闭、宕机等)下的数据丢失。RabbitMQ 的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。
交换器的持久化:
交换器的持久化是通过在声明队列是将durable参数置为true实现的。如果交换器不设置持久化,那么在RabbitMQ服务重启之后,相关的交换器元数据会丢失, 不过消息不会丢失,只是不能将消息发送到这个交换器中了。对一个长期使用的交换器来说, 建议将其置为持久化的。
队列的持久化:
队列的持久化是通过在声明队列时将durable参数置为true实现的。如果队列不设置持久化,那么在RabbitMQ服务重启之后,相关队列的元数据会丢失, 此时数据也会丢失。
消息的持久化:
队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的 消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。通过将消息的投递模式 (BasicProperties中的deliveryMode属性)设置为2即可实现消息的持久化。
设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息依旧存在。单单只设置队 列持久化,重启之后消息会丢失;单单只设置消息的持久化,重启之后队列消失,继而消息也 丢失。单单设置消息持久化而不设置队列的持久化显得毫无意义
注意要点:
可以将所有的消息都设置为持久化,但是这样会严重影响RabbitMQ的性能(随机)。写入 磁盘的速度比写入内存的速度慢得不只一点点。对于可靠性不是那么高的消息可以不采用持久 化处理以提高整体的呑吐量。在选择是否要将消息持久化时,需要在可靠性和吐呑量之间做一 个权衡。
将交换器、队列、消息都设置了持久化之后就能百分之百保证数据不丢失了吗?答案是否 定的。
首先从消费者来说,如果在订阅消费队列时将autoAck参数设置为true,那么当消费者接 收到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。这种情况很好解决,将 autoAck参数设置为false,并进行手动确认。
次,在持久化的消息正确存入RabbitMQ之后,还需要有一段时间(虽然很短,但是不 可忽视)才能存入磁盘之中。RabbitMQ并不会为每条消息都进行同步存盘(调用内核的fsync 方法)的处理,可能仅仅保存到操作系统缓存之中而不是物理磁盘之中。如果在这段时间内 RabbitMQ服务节点发生了宕机、重启等异常情况,消息保存还没来得及落盘,那么这些消息会丢失。
这个问题怎么解决呢?这里可以引入RabbitMQ的镜像队列机制(详细参考9.4节),相当 于配置了副本,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave), 这样有效地保证了高可用性,除非整个集群都挂掉。虽然这样也不能完全保证RabbitMQ消息 不丢失,但是配置了镜像队列要比没有配置镜像队列的可靠性要高很多,在实际生产环境中的 关键业务队列一般都会设置镜像队列。
还可以在发送端引入事务机制或者发送方确认机制来保证消息己经正确地发送并存储至 RabbitMQ中,前提还要保证在调用channel_.basicPublish方法的时候交换器能够将消息 正确路由到相应的队列之中。
在使用RabbitMQ的时候,可以通过消息持久化操作来解决因为服务器的异常崩溃而导致 的消息丢失,除此之外,我们还会遇到一个问题,当消息的生产者将消息发送出去之后,消息 到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返 回任何信息给生产者的,也就是默认情况下生产者是不知道消息有没有正确地到达服务器。如 果在消息到达服务器之前己经丢失,持久化操作也解决不了这个问题,因为消息根本没有到达 服务器,何谈持久化?
RabbitMQ针对这个问题,提供了两种解决方式:
通过事务机制实现;
通过发送方确认(publisherconfirm)机制实现。
RabbitMQ客户端中与事务机制相关的方法有三个:channel. txSelect、 channel .txCommit 和 channel .txRollback。
channel .txSelect 用于将当前的信道设置成事务模式。
channel.txCommit用于提交事务。
channel.txRollback用于事务回滚。
在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ 了, 如果事务提交成功,则消息一定到达了 RabbitMQ中,如果在事务提交执行之前由于RabbitMQ 异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行 channel.txRollback方法来实现事务回滚。
try {
channel.txSelect();
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
int result =1/0;
channel.txCoramit();
} catch (Exception e) {
e.printStackTrace();
channel.txRollback();
}
如果要发送多条消息,则将channel .basicPublish和channel. txCommit等方法包 裹进循环内即可.
事务确实能够解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被 RabbitMQ接收,事务才能提交成功,否则便可在捕获异常之后进行事务回滚,与此同时可以进 行消息重发。但是使用事务机制会“吸干” RabbitMQ的性能,那么有没有更好的方法既能保证 消息发送方确认消息己经正确送达,又能基本上不带来性能上的损失呢?从AMQP协议层面来 看并没有更好的办法,但是RabbitMQ提供了一个改进方案,即发送方确认机制!
采用事务机制实现会严重降低RabbitMQ的消息吞吐量,这里就引入了一种轻量级的方式 发送方确认(publisher confirm)机制。
生产者将信道设置成confirm (确认)模式,一旦信道进入confirm模式,所有在该信道上 面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后, RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一 ID),这就使得生产 者知晓消息己经正确到达了目的地了。如果消息和队列是可持久化的,那么确认消息会在消息 写入磁盘之后发出。RabbitMQ回传给生产者的确认消息中.的deliveryTag包含了确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示到 这个序号之前的所有消息都已经得到了处理。
事务机制在一条消息发送之后会使发送端阻塞,以等待RabbitMQ的回应,之后才能继续 发送下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息, 生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之 后,生产者应用程序便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错 误导致消息丢失,就会发送一条nack (Basic.Nack)命令,生产者应用程序同样可以在回调 方法中处理该nack命令。
生产者通过调用channel.confirmSelect方法(即Confirm.Select命令)将信道设置为confirm模式,之后RabbitMQ会返回Confirm. Select-Ok命令表7K同意生产者将当 前信道设置为confirm模式。所有被发送的后续消息都被ack或者nack—次,不会出现一条消 息既被ack又被nack的情况,并且RabbitMQ也并没有对消息被confirm的快慢做任何保证。
try {
channel. confirmSelect ();//将信道置为 publisher confirm 模式
//之后正常发送消息
channel.basicPublish("exchange", "routingKey", null, "publisher confirm test".getBytes());
if (!channel.waitForConfirms()) {
System.out.println("send message failed");
// do something else....
}
} catch (InterruptedException e) {
e.printStackTrace();
}
如果发送多条消息,只需要将channel.basicPublish和channel. waitFor Confirms方法包裹在循环里面即可,可以参考事务机制,不过不需要把 channel. confirmSelect方法包裹在循环内部。
如果信道没有开启publisher confirm模式,则调用任何waitForConfirms方法都会报出 java .lang. IllegalStateException。对于没有参数的 waitForConfirms 方法来说, 其返回的条件是客户端收到了相应的Basic .Ack/ .Nack或者被中断。参数timeout表示超 时时间,一旦等待RabbitMQ回应超时就会拋出java. util. concurrent. TimeoutException 的异常。 waitForConfirmsOrDie 方法在接收到 RabbitMQ 返回 的Basic .Nack之后会抛出java . io . IOException。业务代码可以根据自身的特性灵活地 运用这四种方法来保障消息的可靠发送。
publisher confirm模式是每发送一条消息后就 调用channel.waitForConfirms方法,之后等待服务端的确认,这实际上是一种串行同步 等待的方式。事务机制和它一样,发送消息之后等待服务端确认,之后再发送消息。两者的存 储确认原理相同,尤其对于持久化的消息来说,两者都需要等待消息确认落盘之后才会返回(调 用Linux内核的fsync方法)。在同步等待的方式下,publisher confirm机制发送一条消息需要通 信交互的命令是2条:Basic. Publish和Basic .Ack;事务机制是3条:Basic. Publish、 Tx.Commmit/.Commit—〇k (或者 Tx.Rollback/.Rollback-〇k),事务机制多了——个命 令帧报文的交5,所以QPS会略微下降。
注意要点:
1.事务机制和publisher confirm机制两者是互斥的,不能共存。如果企图将已开启事务模式 的信道再设置为 publisher confirm 模式,RabbitMQ 会报错,反之亦然。
2.事务机制和publisher confirm机制确保的是消息能够正确地发送至RabbitMQ,这里的 “发送至RabbitMQ”的含义是指消息被正确地发往至RabbitMQ的交换器,如果此交换器没有 匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有 匹配的队列。更进一步地讲,发送方要配合mandatory参数或者备份交换器一起使用来提高 消息传输的可靠性。
publisher confirm的优势在于并不一定需要同步确认。这里我们改进了一下使用方式,总结 有如下两种:
1.批量confirm方法:每发送一批消息后,调用channel.waitForConfirms方法,等 待服务器的确认返回。
相比于前面示例中的普 通confirm方法,批量极大地提升了 confirm的效率,但是问题在于出现返回Basic.Nack或 者超时情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且 当消息经常丢失时,批量confirm的性能应该是不升反降的。
2.异步confirm方法:提供一个回调方法,服务端确认了一条或者多条消息后客户端会回 调这个方法进行处理。
在客户端Channel接口中提供的 addConf irmListener方法可以添加ConfirmListener这个回调接口,这个 ConfirmListener接口包含两个方法:handleAck和handleNack,分别用来处理 RabbitMQ回传的Basic. Ack和Basic. Nack。在这两个方法中都包含有一个参数 deliveryTag (在publisher confirm模式下用来标记消息的唯一有序序号)。我们需要为每一个信道维护一个“unccmfirm”的消息序号集合,每发送一条消息,集合中的元素加1。每当调 用ConfirmListener中的handleAck方法时,“unconfirm”集合中删掉相应的一条 (multiple设置为false)或者多条(multiple设置为true)记录。从程序运行效率上来看, 这个“unconfirm”集合最好采用有序集合SortedSet的存储结构。事实上,Java客户端SDK 中的waitForConfirms方法也是通过SortedSet维护消息序号的。
当RabbitMQ队列拥有多个消费者时,队列收到的消息将以轮询(round-robin)的分发方式 发送给消费者。每条消息只会发送给订阅列表里的一个消费者。这种方式非常适合扩展,而且 它是专门为并发程序设计的。如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可。
很多时候轮询的分发机制也不是那么优雅。默认情况下,如果有n个消费者,那么RabbitMQ 会将第m条消息分发给第m%n(取余的方式)个消费者,RabbitMQ不管消费者是否消费并己经确认(Basic.Ack) 了消息。试想一下,如果某些消费者任务繁重,来不及消费那么多的消 息,而某些其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了 所分配到的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降。
那么该如何处理这种情况呢?这里就要用到channel.basicQos (int prefetchCount) 这个方法,如前面章节所述,channel.basicQos方法允许限制信道上的消费者所能保持的最大未确认消息的数量。
举例说明,在订阅消费队列之前,消费端程序调用了 channel.basiCQ〇S(5),之后订 阅了某个队列进行消费。RabbitMQ会保存一个消费者的列表,每发送一条消息都会为对应的消 费者计数,如果达到了所设定的上限,那么RabbitMQ就不会向这个消费者再发送任何消息。 直到消费者确认了某条消息之后,RabbitMQ将相应的计数减1,之后消费者可以继续接收消息, 直到再次到达计数上限。这种机制可以类比于TCP/IP中的“滑动窗口”。
注意要点:
Basic.Qos的使用对于拉模式的消费方式无效。
channel .basicQos有三种类型的重载方法:
当prefetchCount设置为0则表示 没有上限。还有prefetchSize这个参数表示消费者所能接收未确认消息的总体大小的上限, 单位为B,设置为0则表示没有上限。
对于一个信道来说,它可以同时消费多个队列,当设置了 prefetchCount大于0时,这个 信道需要和各个队列协调以确保发送的消息都没有超过所限定的prefetchCount的值,这样会 使RabbitMQ的性能降低,尤其是这些队列分散在集群中的多个Broker节点之中。RabbitMQ为了 提升相关的性能,在AMQP 0-9-1协议之上重新定义了 global这个参数。
对于同一个 信道上的多个消费者而言,如果设置了 prefetchCount的值,那么都会生效。
channel.basicQos(3, false); // Per consumer limit
channel.basicQos(5, true); // Per channel limit
channel.basicConsume("queuel", false, consumerl);
channel.basicConsume("queue2n, false, consumer2);
如果在订阅消息之前,既设置了 global为true的限制,又设置了 global为false的跟制。RabbitMQ会确保两者都会生效,举例说明这里每个消费者最多只能收到3个未确认的消息,两个消费者能收到的未确认的消息 个数之和的上限为5。在未确认消息的情况下,如果consumed接收到了消息1、2和3,那么 consumer2至多只能收到11和12。如果像这样同时使用两种global的模式,则会增加RabbitMQ 的负载,因为RabbitMQ需要更多的资源来协调完成这些限制。如无特殊需要,最好只使用 global为false的设置,这也是默认的设置。
消息的顺序性是指消费者消费到的消息和发送者发布的消息的顺序是一致的。举个例子, 不考虑消息重复的情况,如果生产者发布的消息分别为msgl、msg2、msg3,那么消费者必然 也是按照msgl、msg2、msg3的顺序进行消费的。
目前很多资料显示RabbitMQ的消息能够保障顺序性,这是不正确的,或者说这个观点有 很大的局限性。在不使用任何RabbitMQ的高级特性,也没有消息丢失、网络故障之类异常的 情况发生,并且只有一个消费者的情况下,最好也只有一个生产者的情况下可以保证消息的顺 序性。如果有多个生产者同时发送消息,无法确定消息到达Broker的前后顺序,也就无法验证 消息的顺序性。
那么哪些情况下RabbitMQ的消息顺序性会被打破呢?下面介绍几种常见的情形。
如果生产者使用了事务机制,在发送消息之后遇到异常进行了事务回滚,那么需要重新补 偿发送这条消息,如果补偿发送是在另一个线程实现的,那么消息在生产者这个源头就出现了 错序。同样,如果启用publisher confirm时,在发生超时、中断,又或者是收到RabbitMQ的 Basic.Nack命令时,那么同样需要补偿发送,结果与事务机制一样会错序。或者这种说法有 些牵强,我们可以固执地认为消息的顺序性保障是从存入队列之后开始的,而不是在发送的时候开始的。
考虑另一种情形,如果生产者发送的消息设置了不同的超时时间,并且也设置了死信队列, 整体上来说相当于一个延迟队列,那么消费者在消费这个延迟队列的时候,消息的顺序必然不 会和生产者发送消息的顺序一致。
再考虑一种情形,如果消息设置了优先级,那么消费者消费到的消息也必然不是顺序性的。
如果一个队列按照前后顺序分有msgl、msg2、msg3、msg4这4个消息,同时有ConsumerA 和ConsumerB这两个消费者同时订阅了这个队列。队列中的消息轮询分发到各个消费者之中, ConsumerA 中的消息为 msgl 和 msg3,ConsumerB 中的消息为 msg2、msg4。ConsumerA 收到 消息msgl之后并不想处理而调用了 Basic.Nack/.Reject将消息拒绝,与此同时将 requeue设置为true,这样这条消息就可以重新存入队列中。消息msgl之后被发送到了 ConsumerB中,此时ConsumerB己经消费了 msg2、msg4,之后再消费msgl,这样消息顺序性 也就错乱了。或者消息msgl又重新发往ConsumerA中,此时ConsumerA己经消费了 msg3, 那么再消费msgl,消息顺序性也无法得到保障。同样可以用在Basic.Recover这个AMQP 命令中。
包括但不仅限于以上几种情形会使RabbitMQ消息错序。如果要保证消息的顺序性,需要 业务方使用RabbitMQ之后做进一步的处理,比如在消息体内添加全局有序标识(类似Sequence ID)来实现。
消息可靠传输一般是业务系统接入消息中间件时首要考虑的问题,一般消息中间件的消息 传输保障分为三个层级。
1.At most once:最多一次。消息可能会丢失,但绝不会重复传输。
2.At least once:最少一次。消息绝不会丢失,但可能会重复传输。
3.Exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。
RabbitMQ支持其中的“最多一次”和“最少一次”。其中“最少一次”投递实现需要考虑 以下这个几个方面的内容:
“最多一次”的方式就无须考虑以上那些方面,生产者随意发送,消费者随意消费,不过这 样很难确保消息不会丢失。
“恰好一次”是RabbitMQ目前无法保障的。考虑这样一种情况,消费者在消费完一条消息 之后向RabbitMQ发送确认Basic. Ack命令,此时由于网络断开或者其他原因造成RabbitMQ 并没有收到这个确认命令,那么RabbitMQ不会将此条消息标记删除。在重新建立连接之后, 消费者还是会消费到这一条消息,这就造成了重复消费。再考虑一种情况,生产者在使用 publisher confirm机制的时候,发送完一条消息等待RabbitMQ返回确认通知,此时网络断开, 生产者捕获到异常情况,为了确保消息可靠性选择重新发送,这样RabbitMQ中就有两条同样 的消息,在消费的时候,消费者就会重复消费。
那么RabbitMQ有没有去重的机制来保证“恰好一次”呢?答案是并没有,不仅是RabbitMQ, 目前大多数主流的消息中间件都没有消息去重机制,也不保障“恰好一次”。去重处理一般是在 业务客户端实现,比如引入GUID (Globally Unique Identifier)的概念。针对GUID,如果从客 户端的角度去重,那么需要引入集中式缓存,必然会增加依赖复杂度,另外缓存的大小也难以 界定。建议在实际生产环境中,业务方根据自身的业务特性进行去重,比如业务消息本身具备 幂等性,或者借助Redis等其他产品进行去重处理。