【RabbitMQ】消息队列中间件学习之RabbitMQ(5)

消息何去何从?

在正常情况下,生产者产生并发送一条消息然后被交换器正确路由到某个队列中。但是如果一条消息不能被正确路由到某个队列时,那么这条消息该何去何从呢?RabbitMQ提供以下几个处理方案
(1)将消息返回给生产者;
(2)直接将消息丢失;
(3)使用备份交换器将未能被路由的消息存储起来。

mandatoryimmediate是AMQP协议中basic.publish方法中的两个标识位,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。RabbitMQ提供的备份交换器(Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存储起来,而不用返回给客户端。

mandatory参数

当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返回给生产者(Basic.Return + Content-Header + Content-Body);当mandatory设置为false时,出现上述情形broker会直接将消息扔掉。

那么生产者如何获取到没有被正确路由到合适队列的消息呢?可以通过channel.addReturnListener来添加ReturnListener监听器实现。关键代码如下:

channel.basicPublish(EXCHANGE_NAME,“”,true,MessageProperties.PERSISTENT_TEXT_PLAIN,“mandatory test”.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);
	}
});

immediate参数

当immediate参数设置为true时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回至生产者

概括来说**,mandatory参数告诉服务器至少将该消息路由到一个队列中,否则将消息返还给生产者**;immediate参数告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了

在RabbitMQ 3.0开始的版本去掉了对immediate参数的支持,对此官方解释是:immediate参数会影响镜像队列的性能,增加了代码的复杂性,建议采用TTL和DLX的方法替代。

备份交换器

备份交换器(Alternate Exchange,简称AE)。生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失;如果设置了mandatory参数,需要添加ReturnListener的编程逻辑,生产者的代码将变得复杂。如果不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在RabbitMQ中,再在需要的时候处理这些消息

可以通过在声明交换器(调用cannel.exchangeDeclare方法)的时候添加alternate-exchange参数来实现,也可以通过策略(Policy)的方式实现。如果两者同时使用,则前者的优先级更高,会覆盖掉Policy设置。关键代码如下:

Map<String, Object> params = new HashMap<>();
params.put("alternate-exchange", "myAe");
channel.exchangeDeclare("normalExchange", "direct", true, false, params);
channel.exchangeDeclare("myAe", "fanout", true, false, null);
channel.queueDeclare("normalQueue", true, false, false, null);
channel.queueBind("normalQueue", "normalExchange", "normalKey");
channel.queueDeclare("unroutedQueue", true, false, false, null);
channel.queueBind("unroutedQueue", "myAe", "");

上面的代码声明了两个交换器normalExchange和myAe,分别绑定了normalQueue和unroutedQueue这两个队列,同时将myAe设置为normalExchange的备份交换器,注意myAe的交换器类型为fanout。
备份交换器
同样地,如果采用Policy的方式来设置备份交换器,可以参考如下代码:

rabbitmqctl set_policy AE "^normalExchange$" '{"alternate-exchange":"myAE"}'

备份交换器其实和普通的交换器没有太大的区别,为了方便使用,建议设置为fanout类型,当然使用其他类型也可以。
对于备份交换器,总结了以下几种特殊情况:

  • 如果设置的备份交换器不存在,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器没有绑定任何队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器没有任何匹配的队列,客户端和RabbitMQ服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。

过期时间(TTL)

TTL,Time to Live,即过期时间。RabbitMQ可以对消息和队列设置TTL。

设置消息的TTL

目前有两种方法可以设置消息的TTL

  • 通过队列属性设置,队列中所有消息都有相同的过期时间。
  • 对消息本身进行单独设置,每条消息的TTL可以不同。

如果两种方法一起使用,则消息的TTL以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的TTL值时,就会变成“死信(Dead Message)”,消费者将无法再收到该消息(不是绝对的)。

  1. 通过队列属性设置消息TTL是在channel.queueDeclare方法中加入x-message-ttl参数实现的,其单位是毫秒,代码如下:
Map<String, Object> paramss = new HashMap<>();
paramss.put("x-message-ttl", 6000);
channel.queueDeclare("queueName", false, false, false, paramss);

或者是:

var arg = new ConcurrentDictionary<string, object>();
arg.TryAdd("x-message-ttl", 6000);
channel.QueueDeclare(QueueName, false, false, false, arg);

同样也可以通过Policy的方式来设置TTL,示例如下:

rabbitmqctl set_policy TTL ".*" '{"message-ttl":6000}' --apply-to queues

还可以调用HTTP API接口设置:

$curl -i -u root:root -H "Content-type:application/json" -X PUT -d '{"auto_delete":false,"durable":true,"arguments":{"x-message-ttl":6000}}' http://localhost:15672/api/queues/{vhost}/{queuename}

如果不设置TTL,则标识此消息不会过期;如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃,这个特性可以部分替代RabbitMQ 3.0版本之前的immediate参数,之所以部分替代,是因为immediate参数在投递失败时会用Basic.Return将消息返回(这个功能可以用死信队列来实现)。

  1. 针对每条消息设置TTL的方法是在channel.basicPublish方法中加入expiration的属性参数,单位为毫秒,关键代码如下:
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(2);//持久化消息
builder.expiration("6000");// 设置TTL=6000ms
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName, routintkey, properties, "ttlTestMessage".getBytes());

还可以通过HTTP API接口设置:

$curl -i -u admin:admin -H "content-type:application/json"  -X POST -d'{"properties":{"expiration":"6000"},"routing_key":"routingkey","payload":"my body","payload_encoding":"string"}'  http://localhost:15672/api/exchanges/{vhost}/{exchangename}/publish

两种方式对比:对于第一种设置队列TTL属性的方法,一旦消息过期,就会从队列中抹去,而第二种方法里,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的

为什么两者得处理方法不一致?因为第一种方法里,队列中已过期的消息肯定在队列头部,RabbitMQ只要定期从队头开始扫描是否有过期消息即可,而第二种方法里,每条消息的过期时间不同,如果要删除所有过期消息,势必要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期,再进行删除

设置队列的TTL

通过设置channel.queueDeclare方法中的x-expires参数可以控制队列被自动删除前处于未使用状态的时间,即队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间内也未调用过Basic.Get方法。

设置队列里的TTL可以应用于类似RPC方式的回复队列,在RPC中,许多队列会被创建出来,但是却是未被使用的。RabbitMQ会确保在过期时间到达后将队列删除,但不保障有多及时,RabbitMQ重启后持久化队列的过期时间会被重新计算

用于表示过期时间的x-expires参数以毫秒为单位,并且服从和x-message-ttl一样的约束条件,不过不能设置为0。关键代码如下:

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-expires", 6000);
channel.queueDeclare("myqueue", false, false, false, args);

死信队列

DLX,Dead-Letter-Exchange,可以称之为死信交换器,当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列

消息变成死信一般是由于以下几种情况:①消息被拒绝(Basic.Reject/Basic.Nack),并且设置requeue参数为false;②消息过期;③队列到达最大长度。

DLX和一般的交换器没有区别,它能在任何队列上被指定,实际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消息以进行相应的处理,这个特性与将消息的TTL设置为0配合使用可以弥补immediate参数的功能。

通过在channel.queueDeclare方法中设置x-dead-letter-exchange参数来为这个队列添加DLX。关键代码如下:

channel.exchangeDeclare("dlx_exchange", "direct");
Map<String, Object> param = new HashMap<>();
param.put("x-dead-letter-exchange", "dlx_exchange");
//为队列myqueue添加DLX
channel.queueDeclare("myqueue", false, false, false, param);

也可以为这个DLX指定路由键,如果没有特殊指定,则使用原队列的路由键:

param.put("x-dead-letter-routing-key","dlx-routing-key");

这里也可以通过Policy的方式设置:

rabbitmqctl set_policy DLX ".*" '{"dead-letter-exchange":"dlx_exchange"}' --apply-to queues

下面是创建一个队列,为其设置TTL和DLX的代码示例:

channel.exchangeDeclare("exchange.dlx", "direct",true);
channel.exchangeDeclare("exchange.normal","fanout",true);
Map<String, Object> params = new HashMap<>();
params.put("x-message-ttl", "1000");
params.put("x-dead-letter-exchange","exchange.dlx");
params.put("x-dead-letter-routing-key","routingkey");
channel.queueDeclare("queue.normal", true, false, false, params);
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());

对于RabbitMQ来说,DLX是一个非常有用的特性,它可以处理异常情况下,消息不能被消费者正确消费而被置于死信队列中情况,后续可以分析死信队列中的内容来分析解决异常情况,进而可以改善和优化系统。DLX配合TTL使用还可以实现延迟队列的功能。

延迟队列

延迟队列存储的对象是对应的延迟消息,即所谓"延迟消息"是指当消息被发送后,并不像让消费者立即拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费,其应用场景可以对应下单后N分钟内未支付,智能电器在指定的时间后执行工作等情况。

AMQP协议中并没有直接支持延迟队列的功能,但是通过前面介绍的DLX和TTL可以模拟出延迟队列的功能。生产者可以将消息发送到与交换器绑定的不同队列中,同时配置DLX和相应的死信队列,当消息过期时会转存到相应的死信队列中,消费者则可以订阅这些死信队列(即所谓的延迟队列)进行消费。

在真实应用中,对于延迟队列可以根据延迟时间的长短分为多个等级,一般分为5秒、10秒、30秒、1分钟、5分钟、10分钟、30分钟、1小时这几个维度,当然也可以再细化。

优先级队列

优先级队列,顾名思义,具有高优先级的队列具有高的优先权,即具备优先被消费的特权。可以通过设置队列的x-max-priority参数实现,示例代码如下:

Map<String, Object> paramss = new HashMap<>();
paramss.put("x-max-priority",10);
channel.queueDeclare("queue.priority", true, false, false, paramss);

然后,需要在发送时在消息中设置消息当前的优先级,示例:

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中没有消息堆积的情况下,对发送的消息设置优先级就没有什么意义。

参考资料:

  1. 《RabbitMQ实战指南》 朱忠华 著
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镰刀韭菜

看在我不断努力的份上,支持我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值