写点什么

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

用户头像
LSJ
关注
发布于: 2020 年 06 月 13 日

有天下午,同事问了一个问题:有没有遇到过带 TTL 的消息不能自动过期导致该条消息无法被路由到死信队列的问题。


当我听到这个问题时,脑子里其实是空荡荡的就像被捅了的马蜂窝一样,仿佛再次置身于一场面试中,而自己作为应聘者再次被面试官问了一个自己毫无头绪的问题。我本可说出"没有"二字来答复对方 但是为了让自己不至于太尴尬,所以镇定自若的说了一句:“没有遇到过哦”。努力将"没有"二字延长到"没有遇到过哦"六个字,似乎这样可以弥补自己技术上的不足。

需求的这样的:


要实现的功能很简单,在管理后台实现定时发送 消息推送 的功能。具体发送的时间点可以随意设置。


介于此,有关开发人员决定要使用 RabbitMQ 的 TTL 和死信队列的特性来实现延迟队列,然后就紧张旗鼓的开始做,当然要做的功能多的数也数不清,这只是其中一个,所以一直到上线前夕才抛出开头提到的问题,就像在信心满满的代码中抛出一个迄今为止都没有遇到过的异常一样,顿时茫然无措


要给我做的话,二话不说肯定是上 spring 的定时器,我并没有使用过 RabbitMQ 延迟队列的特性,定时器的话常规套路,熟门熟路,稳打稳扎,是最保险的一种做法,不过也正因为此,一味求稳却缺乏创新,导致技术提升缓慢。

问题是这样的:


每当有一场比赛的时候,在比赛开始前总会给特定用户推送一条消息。所以会把带有 TTL 的消息放入普通队列(起名为 GeneralQ),静静等候(TTL 的值就是自消息入队后开始直到准备要推送消息的那一刻的时间差)。等到消息过期时,通过 DLX(死信交换机)将过期的消息路由到与之关联的 DLQ 中(死信队列),在 DLQ 的那一头,连接着的是一头微伏的消费者,静候其中,一旦有 DLM(死信消息)过来,他便猛扑上去。


经过一番调试,总会有一部分本该过期的消息没有正常到达死信队列。“出现这种情况的概率大不大”,我问道,依然想通过这种问题来掩饰自己的不足,这样的问题再一次完美的暴露了自己的无知和见识短浅,“挺常见的”,他说。我又一次假装陷入沉思。其实我并不是一个不求上进不思进取之人。


然后我开始查找资料,我先是查 TTL,再查死信队列,再查延迟队列;此时:


另一个同事给他仍过去一张截图,说:"你看,给每个消息单独设置 TTL 确实不能及时丢弃消息,而在队列上设置一个全局的 TTL 是可以,我们可以试试这么做"。而这张图片就像闷热的午后三点想起的一声惊雷一样,完全惊醒了我


“能不能把连接发过来,不要只发从上面截取的一张图片”。


“好的”,同事说


而全局设置 TTL 的方式,经过短暂讨论一票否决。


突然间我恍然大悟,过期消息只有在给消费者下发的时候才会丢弃。或者说只有当到达队头的时候才会被丢弃,我突然间又想到了 redis 对于过期健的处理方式,做的异曲同工,只有用的时候才会判断,当然 redis 并不是只有这一种处理方式。


考虑一种情况:第一条入队的消息过期时间是 2 个小时,第二条入队的消息过期时间是 2 分钟,也就是说入队时间是按 TTL 倒叙入队,此时就会陷入一种尴尬之地,由于第一条消息拥有很长的存活时间,它会一直霸占着队首,使得第二条消息在 2 分钟后不能被清理,因为它没在队首,所以没有机会被丢弃。(基于此,我提出了第一个解决方案,后面说)


鉴于此,我想到了队列的顺序问题,自然地想到了优先级队列,所以后面我又迷失在 TTL、死信、 优先级队列这三个概念的混乱中长达 11 个小时之久


再考虑第二种情况:如果将上面的消息按 TTL 时间正序入队,那么是不会出现问题的。


我将这一结论告知于他,完美的解释了他的问题。他将信将疑的做了几次实践,结果完全吻合,充分证明实践出真知。


简短总结

我们总是想当然的以为时间一到便自动清理,就和闹钟一样,时间一到,立刻响起。但其实它就像在冰箱里过期了的馒头,不吃的时候你不知道它发霉没有,等到吃的时候,才拿起来随手扔到垃圾桶。技术不能想当然,而是需要不折不扣的去实践,当然最后可能以失败告终,就像这次我后续的研究结果一样, 但是这并不妨碍我们在实践的过程中学到很多知识。并且失败了以后你才能知道怎样做才能成功,才能理解某一个技术点的边界所在。


套用《Java 编程思想》作者的一句话:只有知道某个技术不能做什么你才能更好的做到所能做的(部分原因是不必浪费时间在死胡同里转)。


显然我依然在很大程度上并没有搞清楚利用死信队列来实现延迟队列的边界所在,所以我依然不甘心,继续前行。而此时我们的讨论告一段落。


不过基于上面的讨论我也提出了一种不成熟的解决方案,那就是再来一头微伏的消费者去监听我上面说的 GeneralQ 队列,不过不要真正的去消费,而是先判断时间有没有过期,没过期在重新 入队 到队列中,如果已经过期,那就已经推到 DLQ 中去了。这种方式猛然一听挺像回事,但是实现起来却还有几个坑去填


剩下的以后在写吧。


“感觉时间过的好快 ”


“其实也没那么快”


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

LSJ

关注

微笑面对每一天 2018.11.11 加入

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

评论 (6 条评论)

发布
用户头像
我之前用redis过期健实现,测试环境是一台redis,到了生产环境是两台,导致会收到两次消息的推送,不知用redis分布式锁怎么实现。这个redis过期我要删除它,我怎么给它加锁呢
2020 年 06 月 17 日 15:56
回复
你是监听了redis键的过期事件吗?收到两次通知要在消费端去做幂等处理,而且使用这种方式实现的话应该也是会出现通知的延迟。最好还是用带有延迟队列特性的消息队列去实现类似的功能
2020 年 06 月 17 日 17:00
回复
用户头像
2020 年 06 月 16 日 14:38
回复
嗯 后续会使用这个插件
2020 年 06 月 16 日 16:20
回复
用户头像
感觉在读小说^_^
2020 年 06 月 16 日 11:54
回复
谢谢支持 谢谢
2020 年 06 月 16 日 13:02
回复
没有更多了
由一次管理后台定时推送功能引发的对RabbitMQ延迟队列的思考(一)