RabbitMQ - 高级地发送消息

465 阅读2分钟

定制消息发送

上文在《RabbitMQ Client 怎么用?》中,提到通过 Channel 类的 basicPublish 方法推送消息。

而提供的最完整的方法如下:

void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body) throws IOException;

其中两个比较特殊的参数:mandatory 和 immediate :

  • mandatory:true:如果 exchange 根据自身类型和消息 routeKey 无法找到一个符合条件的 queue,那么会调用 basic.return 方法将消息返还给生产者。false:出现上述情形 broker 会直接将消息扔掉。

    可以通过添加 ReturnListener 来监听到消息的return。

  • immediate:true:如果exchange在将消息 route 到 queue(s) 时发现对应的 queue 上没有消费者,那么这条消息不会放入队列中。当与消息 routeKey 关联的所有 queue(一个或多个) 都没有消费者时,该消息会通过basic.return 方法返还给生产者。3.0 以后废弃。

immediate 废弃的原因,官方的解释是:该参数会影响镜像队列的性能,增加代码复杂性。建议通过 TTL(过期时间) 和 DLX(死信队列) 替代。

因为如果要检测没有消费者,就需要监听到镜像队列的地步,在引入镜像队列的同时,该参数的处理复杂度就提高了。

备份交换器

Alternate Exchange。上述提到生产者如果发送消息时未设置 mandatory,那么在消息就会被丢掉;如果设置,那么需要添加 ReturnListener 来监听,造成额外的编程成本。

而备份交换器,就是这样一种简化处理,避免消息丢失的处理方案。

在声明交换器(Channel.exchangeDeclare)的时候,通过添加 alternate-exchange参数实现;或者通过策略 Policy 的方式。同时使用时,参数的优先更高。

Map<String, Object> args = new HashMap<>();
args.put("alternate-exchange", "ae");
channel.exchangeDeclare("ae", "fanout", true, false, null);
channel.exchangeDeclare("exchange_demo", "direct", true, false, args);


channel.basicPublish("exchange_demo", "normal", null , "hello world".getBytes());
channel.basicPublish("exchange_demo", "normal" + "_ae", null , "hello world ae".getBytes());

策略方式:

rabbitmqctl set_policy AE "^exchange_demo$" '{"alternate-exchange", "ae"}';

image-20210101220144105

本质上,备份交换器只是普通交换器,只是通过指定参数,将两个交换器进行一种存在“主次”的绑定。只能从主流向次,仅针对一次绑定而言。

ps:当然也可以双写绑定,你中有我,我中有你。只是注意,路由键注定能匹配到一个交换器的队列,否则依然会是丢失。

因此也建议,将备份交换器的类型设置为 "fanout"。

特殊情况
  • 备份交换器不存在,客户端和 Broker 无异常,丢失消息;
  • 备份交换器无绑定队列,同上;
  • 备份交换器无匹配队列,同上;
  • 如果和 mandatory 同时使用,mandatory 失效。其实结果就是丢失消息,因为是不会return了。

请记住备份交换器如果出现特殊情况,不会抛异常,最终的结果必然是丢失消息。

过期时间TTL

RabbitMQ 可以对消息队列设置 TTL。

设置消息

两种方式:

1.通过队列属性设置:将应用在队列中的所有消息。

2.通过消息本身单独设置。

同时使用时,以两者较小的为准。一旦消息超过 TTL,将会成为 死信。如果没有设置死信队列,将无法被消费者收到。

通过队列设置

在声明对了(Channel.queueDeclare)的时候,通过添加 x-message-ttl参数实现;或者通过策略 Policy 的方式。单位是毫秒

Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 6000);
channel.queueDeclare(QUEUE_DEMO, true, true, false, args);

如果TTL 设置为 0,除非当时刚好可以将消息投递到消费者,否则将被丢失。

和 immediate 很像,本质上都只在队列逗留一会。但是 immediate 会将消息返回到生产者。

因此 TTL 的特性可以部分代替 immediate,消息退回可以通过 DLX..

通过消息单独设置

Channel.basicPublish的时候,通过加入 expiration 的参数设置。

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(2);// 持久化消息
builder.expiration("6000");
channel.basicPublish(EXCHANGE_DEMO, ROUTING_KEY, builder.build() , "hello world".getBytes());

通过队列设置的方式,一旦消息ttl到期,就会被队列删除。

消息单独设置的方式,即使消息过期,也不会马上删除,而是等到消息投递时进行检查再放弃投递。

因为第一种方式,队列的过期消息一定在队头,只要定期从队头开始扫描即可;而第二种方式就需要扫描全队。

设置队列

在声明队列(Channel.queueDeclare)的时候,通过添加 x-expires参数实现。

单位依然是毫秒。但是不能设置为0。

概念

队列过期时间是什么概念呢?就是队列自动删除前处于未被使用的时间。

未被使用:

  • 队列没有任何消费
  • 队列没有被重新声明
  • 过期时间段内没有被调用过 Basic.Get。即没有消费者主动拉取消息。
应用场景

可以应用于类似 RPC 的回复队列。在 RPC 中,很多队列会被创建,都却从未被使用。

PS : 比如 dubbo 通过异步转同步的方式。在服务端返回结果入队,通知应用线程去获取。不确定书上说的是不是这个,但是应该也是类似的一种场景。

RabbitMQ 保证过期时间到达后会将队列删除,但不保证及时性。服务器重启后,会重新计算过期时间。

总结

文章其实主要是说明了消息发送时,如果遇到一些特殊情况的处理,比如没有匹配的交换器、队列、甚至是消费者。

过期时间反倒是开发者都熟悉的一个概念,譬如网络协议中,保证消息时效性。

后面在讲讲队列进阶:死信队列、优先级队列及持久化。

参考

《RabbitMQ 实战指南》

本文由博客群发一文多发等运营工具平台 OpenWrite 发布