由一次管理后台定时推送功能引发的对 RabbitMQ 延迟队列的思考 (二)

发布于: 2020 年 06 月 20 日

在RabbitMQ中,带有过期时间的消息并不会在队列中自动丢弃(这也是上篇文章主要说的问题)。所以如果一个队列中的消息都带有TTL值的话,最好能有消费者去订阅此队列,这样就能够保证那些已经过期但还没有被抛弃的消息,能够尽快被丢弃或者被当成死信路由到死信队列中。

一个不成熟的想法

所以我想到要增加一个消费者去订阅GeneralQ(上一篇文章提到过)队列,只不过这个消费者并不真正地处理消息,它的作用只是为了让过期的消息有机会被刷到死信队列中。 为达到此目的,消费者需要循环消费此队列。

消费者在拿到消息以后先要判断时间,如果没过期再将消息重新入队;如果已过期就拒绝消息。循环往复

一般来说消费者是不会接收到已经过期的消息,所以是否还有必要再去判断消息是否过期呢?其实这里主要考虑一种比较极端的竞态条件:消息可能在写入socket之后,在到达消费者之前过期

重新入队/消息丢弃

消息重新入队可以调用basicNack方法,但是需要将requeue参数设置成true

拒绝消息可以调用basicNack或者basicReject两个方法,当然在此种情况下我们拒绝消息的目的其实是想将消息放入死信队列中,所以在调用这两个方法其中一个时,需要将requeue参数设置为false。

两个方法定义如下:

void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
void basicReject(long deliveryTag, boolean requeue) throws IOException;

消息被死信时机

顺便补充一点,能够触发“死信”事件发生的有三个时机:

  1. 消息过期

  2. 队列长度超过了限制,消息被丢弃

  3. 上面提到的拒绝消息并且不重新入队

重新认识requeue操作

一开始,我以为重新入队是将消息原封不动的放入到队尾;基于此种假设,我才想到了上面提到的解决方案,后来当我在官方文档中寻求答案时,才发现重新入队并不是将消息插入队尾,而是尽可能的放回到该消息在队列中原来的位置,基本等同于放到队头。

所以采用这种方式不免陷入尴尬之地:如果在消费端检测到消息未过期,那么执行重新入队操作的话,这个消息马上会被再次送过来(因为依然处在队列头部),循环往复,导致后面那些本该因时间过期而早该丢弃的消息沉沦在前辈们无休止的循环中,无法释放

看到这里,有些许失望,这是一次失败的尝试,像在做一道数学证明题,做到最后一步发现一切都是徒劳。不过我已泥足深陷,不想轻易回头,顺着这个思路,放弃重新入队操作,转而使用重新发送,将消息发送到队尾。此时这个消费者既充当生产者又充当消费者。只不过在重新发布消息时需要重新计算ttl值。所以在第一次生产这条消息的时候又不得不将消息真正的结束时间戳放入消息体中,方便后续的计算。

总之这种方式难登大雅之堂,弃之也属正常,不过也并不能武断的认为是在白白折腾。

又一个不成熟的想法:优先级队列

上面的方法失败以后,我就在想,如果能让消息在队列中按照ttl时间值正序排序的话就完美了,从头到尾互不耽误,处在队头的永远是即将过期的。想到此便兴奋不已。还没来的及进行有效思考,优先级队列便冒然涌现。

本以为抓住了一跟救命稻草,无奈错误的思绪厚重繁杂,这跟稻草根本无力承载。被别人轻轻一拉便断了念想。

第一件要解决的事情是需要将ttl值和优先值进行映射,这点比较麻烦且适用面窄的可怜,因为在严格盘查官方文档时发现 优先级的值最好保持在1-10范围以内,最大值255。数值越大性能消耗越大。但我好歹勉强找到一个平衡点,不过这已是强弩之末。

第二件要确定的事情是消息能否能按照优先级大小在没有消费者订阅的队列中排序?

为了验证这一点,马上开始写单元测试,运行几次,结果都出乎意料的完美。我欣喜若狂,觉得自己发现了一个惊世骇俗的解决方案,于是将这一结论告知于他,他将信将疑,说:我试试。在他的几番调试下,得出了失败的结论。我又倔强的开始严格盘点测试代码,最终发现他是对的。

重新认识优先队列

带有偏执的主观意识总是能够写出虚假迷人的错误代码,自己又无法察觉,被别人轻轻一推便轰然倒塌,就像一座伟岸的宫殿仅靠一根柱子支撑。本次尝试再次以失败告终——优先级队列并不会自动排序。我到官方站点寻求安慰

文档上写明:如果一个饥饿型的消费者连接到一个空队列,消息随后将被发布到该队列,则消息可能根本不会在队列中等待任何时间。在这种情况下,优先队列将没有机会对它们进行优先排序。同时官方也提到一种思路: 在消费者端开启手动确认模式,同时,通过使用basic.qos方式去限制发送的消息数,从而允许队列对消息进行优先级排序,不过这种方式也并不适用于此。

所以事实证明,这种方式并不能如我所愿,只有消费者存在的情况下优先级队列才有可能有用武之地。如果连消费者都不复存在,优先级就更无从谈起。优先级和延迟队列根本就是八竿子打不到一起的两样东西。强行组CP,违和感十足。

作为开发者,在我们的职业生涯中,肯定会无数次的陷入这样的场景,不停尝试,失败,最后得出结论。过程可能刺激不断、精美绝伦,结局也可能不遂人愿,以失败收场。但更重要的是在这一次次的尝试中,我们能够发现什么场景下应该使用哪些技术,不应该使用哪些技术。

最后小结

定时器可以说是最古老的一种手段,市面上很多延迟需求都可满足。

而关于这次我们使用的这种技术手段,适用面不能和正规的延迟队列相比。但却是也有用武之地:比如说消息的ttl时间是自消息入队开始算起,经过固定时间点后过期的。比如,下订单功能,所有用户下的订单在30分钟内,不支付便作废。这种场景可以使用ttl加死信队列实现,更优雅的做法是,将ttl值全局地设置在队列上,而不是给每个消息设置ttl。

最后关于这种定时发布的功能,最合适的还是使用正儿八经的延迟队列。RabbitMQ已然有插件可用,其他队列天然支持。

发布于: 2020 年 06 月 20 日 阅读数: 11
用户头像

LSJ

关注

微笑面对每一天 2018.11.11 加入

一个具有N年编程功力却早已拥有2N年工作经验的boy

评论

发布
暂无评论
由一次管理后台定时推送功能引发的对 RabbitMQ 延迟队列的思考 (二)