定制消息发送
上文在《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"}';
本质上,备份交换器只是普通交换器,只是通过指定参数,将两个交换器进行一种存在“主次”的绑定。只能从主流向次,仅针对一次绑定而言。
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 发布