总结:消息队列的一些特性。
过期时间(TTL)
Time To Live,也就是生存时间,是一条消息在队列中的最大存活时间,单位是毫秒。了解Redis的朋友应该一看就明白,二者很像。
RabbitMQ可以对消息和队列设置TTL。
RabbitMQ支持设置消息的过期时间,在消息发送的时候可以进行指定,每条消息的过期时间可以不同。
RabbitMQ支持设置队列的过期时间,从消息入队列开始计算,直到超过了队列的超时时间配置,那么消息会变成死信,自动清除。
如果两种方式一起使用,则过期时间以两者中较小的那个数值为准。
当然也可以不设置TTL,不设置表示消息不会过期;如果设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息将被立即丢弃。
消息确认
为了保证消息从队列可靠地到达消费者,RabbitMQ提供了消息确认机制。消费者订阅队列的时候,可以指定autoAck参数,当autoAck为true的时候,RabbitMQ采用自动确认模式,RabbitMQ自动把发送出去的消息设置为确认,然后从内存或者硬盘中删除,而不管消费者是否真正消费到了这些消息。当autoAck为false的时候,RabbitMQ会等待消费者回复的确认信号,收到确认信号之后才从内存或者磁盘中删除消息。
消息确认机制是RabbitMQ消息可靠性投递的基础,只要设置autoAck参数为false,消费者就有足够的时间处理消息,不用担心处理消息的过程中消费者进程挂掉后消息丢失的问题。
持久化
消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何保证消息可靠性的呢?答案就是消息持久化。持久化可以防止在异常情况下丢失数据。RabbitMQ的持久化分为三个部分:交换器持久化、队列持久化和消息的持久化。
交换器持久化可以通过在声明队列时将durable参数设置为true。如果交换器不设置持久化,那么在RabbitMQ服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器了。
队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。队列的持久化可以通过在声明队列时将durable参数设置为true。
设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息依然存在。如果只设置队列持久化或者消息持久化,重启之后消息都会消失。
当然,也可以将所有的消息都设置为持久化,但是这样做会影响RabbitMQ的性能,因为磁盘的写入速度比内存的写入要慢得多。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。鱼和熊掌不可兼得,关键在于选择和取舍。在实际中,需要根据实际情况在可靠性和吞吐量之间做一个权衡。
死信队列
当消息在一个队列中变成死信之后,他能被重新发送到另一个交换器中,这个交换器成为死信交换器,与该交换器绑定的队列称为死信队列。。
消息变成死信有下面几种情况:
- 消息被拒绝。通过调用basic.reject或者basic.nack并且设置requeue=false。
- 消息过期
- 队列达到最大长度
DLX也是一个正常的交换器,和一般的交换器没有区别,他能在任何的队列上面被指定,实际上就是设置某个队列的属性。当这个队列中有死信的时候,RabbitMQ会自动将这个消息重新发送到设置的交换器上,进而被路由到另一个队列,我们可以监听这个队列中消息做相应的处理。
死信队列设置:
- 设置死信队列的exchange和queue,然后进行绑定
- Exchange:dlx.exchange
- Queue:dlx.queue
- RoutingKey:#
- 然后进行正常声明交换器、队列、绑定,只不过我们需要在队列上加一个参数即可:arguments.put(“x-dead-letter-exchange”,“dlx.exchange”)
死信队列有什么用?
当发生异常的时候,消息不能够被消费者正常消费,被加入到了死信队列中。后续的程序可以根据死信队列中的内容分析当时发生的异常,进而改善和优化系统。
延迟队列
一般的队列,消息一旦进入队列就会被消费者立即消费。延迟队列就是进入该队列的消息会被消费者延迟消费,延迟队列中存储的对象是的延迟消息,“延迟消息”是指当消息被发送以后,等待特定的时间后,消费者才能拿到这个消息进行消费。
延迟队列用于需要延迟工作的场景。最常见的使用场景:淘宝或者天猫我们都使用过,用户在下单之后通常有30分钟的时间进行支付,如果这30分钟之内没有支付成功,那么订单就会自动取消。除了延迟消费,延迟队列的典型应用场景还有延迟重试。比如消费者从队列里面消费消息失败了,可以延迟一段时间以后进行重试。
基于RabbitMQ实现消息延迟队列方案
延时队列使用场景
在很多的业务场景中,延时队列可以实现很多功能,此类业务中,一般上是非实时的,需要延迟处理的,需要进行重试补偿的。
- 订单超时关闭:在支付场景中,一般上订单在创建后30分钟或1小时内未支付的,会自动取消订单。
- 短信或者邮件通知:在一些注册或者下单业务时,需要在1分钟或者特定时间后进行短信或者邮件发送相关资料的。本身此类业务于主业务是无关联性的,一般上的做法是进行异步发送。
- 重试场景:比如消息通知,在第一次通知出现异常时,会在隔几分钟之后进行再次重试发送。
RabbitMQ实现延时队列
本身在RabbitMQ中是未直接提供延时队列功能的,但可以使用TTL(Time-To-Live,存活时间)和DLX(Dead-Letter-Exchange,死信队列交换机)的特性实现延时队列的功能。
存活时间(Time-To-Live 简称 TTL)
RabbitMQ中可以对队列和消息分别设置TTL,TTL表明了一条消息可在队列中存活的最大时间。当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在TTL时间后死亡成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
死信交换(Dead Letter Exchanges 简称 DLX)
上个知识点也提到了,设置了TTL的消息或队列最终会成为Dead Letter,当消息在一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是DLX,绑定此DLX的队列就是死信队列。
一个消息变成死信一般上是由于以下几种情况;
- 消息被拒绝
- 消息过期
- 队列达到了最大长度。
所以,通过TTL和DLX的特性可以模拟实现延时队列的功能。当队列中的消息超时成为死信后,会把消息死信重新发送到配置好的交换机中,然后分发到真实的消费队列。故简单来说,我们可以创建2个队列,一个队列用于发送消息,一个队列用于消息过期后的转发的目标队列。
SpringBoot集成RabbitMQ实现延时队列实战
以下使用SpringBoot集成RabbitMQ进行实战说明,在进行http消息通知时,若通知失败(地址不可用或者连接超时)时,将此消息转入延时队列中,待特定时间后进行重新发送。
源码地址:
https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-38
RabbitMQ延迟消息的延迟极限是多少?
问题定位
因为不是所有的消息都出现了没有延迟消息效果的因素,通过有问题的消息特征,大致猜测可能是延迟时间过长导致了消息延迟失败。为了验证这个原因,先拿之前文章中的例子,来测试一下延迟时间是否与问题直接相关。
对之前的延迟消息使用样例(文末的Git仓库中可以获取完整代码)接口做一下微改,增加了一个请求参数delay来控制延迟时间:
@GetMapping("/sendMessage")
public String messageWithMQ(@RequestParam String message, @RequestParam Long delay) { log.info("Send: " + message); testTopic.output().send(MessageBuilder.withPayload(message).setHeader("x-delay", delay).build()); return "ok"; }
@GetMapping("/sendMessage")
public String messageWithMQ(@RequestParam String message, @RequestParam Long delay) { log.info("Send: " + message); testTopic.output().send(MessageBuilder.withPayload(message).setHeader("x-delay", delay).build()); return "ok"; }
然后尝试发起了两个请求:
请求1:延迟5000毫秒。消息发送到MQ之后确实延迟了5秒之后才得到了消费,没有任何问题。
curl localhost:8080/sendMessage?message=hello&delay=5000
请求2:延迟1年(31536000000毫秒)。消息发送到MQ之后马上就被消费者消费了,完全没有延迟效果。
curl localhost:8080/sendMessage?message=hello&delay=31536000000
问题小结
在明确了问题原因之后,需要对该功能的时候做一些明确的限定(延迟时间的极限),以避免再次出现类似的问题。深入探索下去,这里的失败主要与消息的过期时间(TTL)有直接的关系。在RabbitMQ中,消息的过期时间必须是非负 32 位整数,即:0 <= n <= 2^32-1,以毫秒为单位。 其中,2^32-1 = 4294967295。
这里我们可以尝试下面两个请求,分别设置延迟时间为4294967295何4294967296:
curl localhost:8080/sendMessage?message=hello&delay=4294967295
curl localhost:8080/sendMessage?message=hello&delay=4294967296
可以发现,当延迟时间为4294967295毫秒的时候,延迟消息工作正常;当延迟时间为4294967296毫秒的时候,消息被直接消费,没有延迟效果。
所以,我们在使用RabbitMQ的延迟消息功能时候,必须注意它的延迟极限是4294967295毫秒(约 49 天)。如果你的业务需求会超过这个临界值,就必须避开这个坑,采用其他方法来实现需要延迟或者定时执行的任务了。