延时队列我在项目里是怎么实现的?

我是3y,一年 CRUD经验用十年的 markdown程序员👨🏻‍💻常年被誉为职业八股文选手

前阵子,有个小伙伴找到问我,如果要实现 延时发送,那是基于什么来做的。

延时队列我在项目里是怎么实现的?

我看到这个问题之后,稍微思考了下,觉得确实也是 austin平台所需要实现的功能。对于前端而言,只要让业务方在创建模板的时候填选屏蔽类型, 后端根据这个字段增添一点点细节,这个需求就做完了,简单

延时队列我在项目里是怎么实现的?

延迟消息如何实现?

延迟消息就是字面上的意思:当接收到消息之后,我需要 隔一段时间进行处理(相对于立马处理,它隔了一段时间,所以他叫延迟消息)。

在原生的JavaDelayQueue供我们去使用,在使用的时候,我们 add进去的队列的元素需要实现 Delayed接口(同时该接口继承了 Comparable接口,所以我们 DelayQueue有序的)

public interface Delayed extends Comparable {
    long getDelay(TimeUnit unit);
}

poll的源码上可以清晰地发现本质上就是在 取数的时候判断了下时间

long delay = first.getDelay(NANOSECONDS);
if (delay

有的人就反驳到: 这不是废话吗?肯定要判断时间啊,不判断时间怎么知道我要延迟的消息什么时候执行

明白了这点之后,我们再来别的方案。因为在生产环境中是不太可能使用JDK原生延迟队列的,它是 没有持久化的,重启就会导致数据丢失。

延时队列我在项目里是怎么实现的?

austin项目使用内存队列去解耦处理数据已经有人提出服务器重启的时候该怎么办,我的解决思路就是通过 优雅关闭服务器这种手段去尽量避免数据丢失,而延迟队列这种就不能这么干了,我们等不了这么久的。

稍微想想还有什么 存储适合当队列且有持久化机制的呢?

答案显而易见: Redis和消息队列(Kafka/ RocketMQ/ RabbmitMQ等)

我们先来看 Redis里提供了一种数据结构叫做 zset,它是 可排序的集合并且Redis原生就支持持久化。 有赞的延迟队列就是基于通过 zset进行设计和存储的。整体架构如下图:

延时队列我在项目里是怎么实现的?

简单理解这张图就是:将需要延迟的消息放置Redis,通过 Timer轮询得到可执行的消息,将可执行的消息放置不同的 Topic供业务方自行消费。

更多的设计思路可以参考有赞的技术原文,这里我不再赘述:https://tech.youzan.com/queuing_delay/

通过 timer去轮询 zset查看是否有可执行的消息是一种思路,也有人通过Redis的 过期回调的姿势也能达到延迟消息的效果(把消息执行的时间定义为 key过期的时间,当 key触发了过期回调,那说明该消息可执行了)。

延时队列我在项目里是怎么实现的?

说完 Redis,我们再来看看消息队列。在 austin项目上使用消息队列是 Kafka,而 Kafka在官方是没有提供延迟队列这种机制的。不过 RabbmitMQRocketMQ都有对应的机制,我们可以简单看看窥探下它们的实现思路。

RabbmitMQ它的延迟队列机制本质上也是通过 TTL(Time To Live 消息存活的时间)所实现的,当队列里的元素触发了过期时,会被送往到 Dead Letter Exchanges(死信队列中)。我们可以将死信队列的元素再次转发,对其进行消费,从而达到延迟队列的效果。

毕竟 RabbmitMQ是专门做消息队列的,所以它对消息的 可靠性会比 Redis更加高(消息投递的可靠性、至少处理一次的消费语义)

延时队列我在项目里是怎么实现的?

RocketMQ支持在我们投递消息的时候设置 延迟等级

Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
// This message will be delivered to consumer 10 seconds later.

message.setDelayTimeLevel(3);
// Send the message
producer.send(message);

默认支持18个延迟等级,分别是:

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

当我们设置了延迟等级的消息之后, RocketMQ不会把消息直接投递到对应的topic,而是 转发到对应延迟等级的队列中。在Broker内部会为每个延迟队列起 TimerTask来进行判断是否有消息到达了时间。

ScheduleMessageService#start

for (Map.Entry entry : this.delayLevelTable.entrySet()) {
    this.deliverExecutorService.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME, TimeUnit.MILLISECONDS);
}

如果到期了,则将消息重新存储到CommitLog,转发到真正目标的 topic

延时队列我在项目里是怎么实现的?

RocketMQ延迟队列比较感兴趣的,推荐看这篇文章:https://cloud.tencent.com/developer/article/1581368

实现需求

在前面提到我们可以利用JDK原生的延时队列,又或是Redis的 zset数据结构或者其过期时间机制、又或是 RabbitMQ使用 TTL+死信队列机制、又或是 RocketMQ的延时等级队列机制来实现我们的 需求(延时队列)

针对此次需求,上面所讲的延时队列,我都没用到…

austin项目引入的是 Kafka,不太可能去为了延时队列去引入第二种消息队列( RabbitMQ在互联网应该用得相对较少, RocketMQ需要改动配置文件的延迟等级才能支持更丰富的延时需求)。

如果基于Kafka或者Redis去 二次开发延时队列,开发成本还是有不少的,在GitHub也还没捞到我想要的轮子。

于是,我换了一种方案: 万物皆扫表

针对这次需求(晚上发的消息,次日早上发送),就不需要上延时队列,因为 austin已经接入了分布式定时任务框架了(对应的实现是 xxl-job

只要把晚上的接收到的消息扔进 Redis list,然后启个定时任务(每天早上9点)轮询该 list是否有数据,如果有再重新做处理就完事了。

延时队列我在项目里是怎么实现的?

延时队列我在项目里是怎么实现的?

总结

这篇文章主要讲述了 如果我们要使用延时队列,我们可以有什么方案,他们的设计是怎么样的。在需求侧上看,这个需求就是「延时队列」的场景,但基于现状的系统架构和开发成本考虑,我们是可以用 另类(分布式定时任务框架)的方式去把需求给实现了。

很多时候,我们看到的系统很烂,技术栈很烂,发现好多场景都没有用到 最佳实践而感到懊恼,在 年轻的时候都想有重构的心。但实际上每引入一个中间件都是需要付出成本的,粗糙也有粗糙的好处。

只要业务能完美支持,那就是好的方案。想要搞自己想搞的技术,那就做开源,如果有一天我觉得分布式定时任务来实现此次需求不顺眼了,我再花时间来重构才干掉,现在就这么实现吧( // TODO)。

如果你实在是觉得看着糟心,欢迎提个 pull request,这样我就不得不把这种实现给干掉了(我对提过来的 pull request都会谨慎且 用心处理)

都看到这里了,点个赞一点都不过分吧?我是3y,下期见。

延时队列我在项目里是怎么实现的?

关注我的微信公众号【 Java3y】除了技术我还会聊点日常,有些话只能悄悄说~ 【对线面试官+从零编写Java项目】 持续高强度更新中!求star!! 原创不易!!求三连!!

延时队列我在项目里是怎么实现的?

austin项目源码Gitee链接:gitee.com/austin

austin项目源码GitHub链接:github.com/austin

Original: https://www.cnblogs.com/Java3y/p/16346958.html
Author: Java3y
Title: 延时队列我在项目里是怎么实现的?

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/594434/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球