关于java:一口气说出-6-种实现延时消息的方案还有谁不会

8次阅读

共计 4490 个字符,预计需要花费 12 分钟才能阅读完成。

原文:juejin.cn/post/6844904150703013901

延时音讯(定时音讯)指的在分布式异步音讯场景下,生产端发送一条音讯,心愿在指定延时或者指定工夫点被生产端生产到,而不是立即被生产。

延时音讯实用的业务场景十分的宽泛,在分布式系统环境下,延时音讯的性能个别会在下沉到中间件层,通常是 MQ 中内置这个性能或者内聚成一个公共根底服务。

本文旨在探讨常见延时音讯的实现计划以及方案设计的优缺点。

实现计划

基于内部存储实现的计划

这里探讨的内部存储指的是在 MQ 自身自带的存储以外又引入的其余的存储系统。

基于内部存储的计划实质上都是一个套路,将 MQ 和 延时模块 辨别开来,延时音讯模块是一个独立的服务 / 过程。延时音讯先保留到其余存储介质中,而后在音讯到期时再投递到 MQ。当然还有一些细节性的设计,比方音讯进入的延时音讯模块时曾经到期则间接投递这类的逻辑,这里不展开讨论。

下述计划不同的是,采纳了不同的存储系统。

基于 数据库(如 MySQL)

基于关系型数据库(如 MySQL)延时音讯表的形式来实现。

CREATE TABLE `delay_msg` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `delivery_time` DATETIME NOT NULL COMMENT '投递工夫',
  `payloads` blob COMMENT '音讯内容',
  PRIMARY KEY (`id`),
  KEY `time_index` (`delivery_time`)
)

通过定时线程定时扫描到期的音讯,而后进行投递。定时线程的扫描距离实践上就是你延时音讯的最小工夫精度。

举荐一个开源收费的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

长处:

  • 实现简略;

毛病:

  • B+Tree 索引不适宜音讯场景的大量写入;

基于 RocksDB

RocksDB 的计划其实就是在上述计划上抉择了比拟适合的存储介质。

RocksDB 应用的是 LSM Tree,LSM 树更适宜大量写入的场景。滴滴开源的 DDMQ 中的延时音讯模块 Chronos 就是采纳了这个计划。

DDMQ 这个我的项目简略来说就是在 RocketMQ 里面加了一层对立的代理层,在这个代理层就能够做一些性能维度的扩大。延时音讯的逻辑就是代理层实现了对延时音讯的转发,如果是延时音讯,会先投递到 RocketMQ 中 Chronos 专用的 topic 中。延时音讯模块 Chronos 生产失去延时音讯转储到 RocksDB,前面就是相似的逻辑了,定时扫描到期的音讯,而后往 RocketMQ 中投递。

这个计划诚实说是一个比拟重的计划。因为基于 RocksDB 来实现的话,从数据可用性的角度思考,你还须要本人去解决多正本的数据同步等逻辑。

长处:

  • RocksDB LSM 树很适宜音讯场景的大量写入;

毛病:

  • 实现计划较重,如果你采纳这个计划,须要本人实现 RocksDB 的数据容灾逻辑;

基于 Redis

再来聊聊 Redis 的计划。上面放一个比较完善的计划。

  • Messages Pool 所有的延时音讯寄存,构造为 KV 构造,key 为音讯 ID,value 为一个具体的 message(这里抉择 Redis Hash 构造次要是因为 hash 构造能存储较大的数据量,数据较多时候会进行渐进式 rehash 扩容,并且对于 HSET 和 HGET 命令来说工夫复杂度都是 O(1))
  • Delayed Queue 是 16 个有序队列(队列反对程度扩大),构造为 ZSET,value 为 messages pool 中音讯 ID,score 为过期工夫(分为多个队列是为了进步扫描的速度)
  • Worker 代表解决线程,通过定时工作扫描 Delayed Queue 中到期的音讯

这个计划选用 Redis 存储在我看来有几点思考。

  • Redis ZSET 很适宜实现延时队列
  • 性能问题,尽管 ZSET 插入是一个 O(logn) 的操作,然而 Redis 基于内存操作,并且外部做了很多性能方面的优化。

然而这个计划其实也有须要斟酌的中央,上述计划通过创立多个 Delayed Queue 来满足对于并发性能的要求,但这也带来了多个 Delayed Queue 如何在多个节点状况下平均调配,并且很可能呈现到期音讯并发反复解决的状况,是否要引入分布式锁之类的并发管制设计?

在量不大的场景下,上述计划的架构其实能够堕落成主从架构,只容许主节点来解决工作,从节点只做容灾备份。实现难度更低更可控。

定时线程查看的缺点与改良

上述几个计划中,都通过线程定时扫描的计划来获取到期的音讯。

定时线程的计划在音讯量较少的时候,会浪费资源,在音讯量十分多的时候,又会呈现因为扫描距离设置不合理导致延时工夫不精确的问题。能够借助 JDK Timer 类中的思维,通过 wait-notify 来节俭 CPU 资源。

获取中最近的延时音讯,而后 wait(执行工夫 - 以后工夫),这样就不须要浪费资源达到工夫时会主动响应,如果有新的音讯进入,并且比咱们期待的音讯还要小,那么间接 notify 唤醒,从新获取这个更小的音讯,而后又 wait,如此循环。

开源 MQ 中的实现计划

再来讲讲目前自带延时音讯性能的开源 MQ,它们是如何实现的

RocketMQ

RocketMQ 开源版本反对延时音讯,然而只反对 18 个 Level 的延时,并不反对任意工夫。只不过这个 Level 在 RocketMQ 中能够自定义的,所幸来说对一般业务算是够用的。默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18 个 level。另外,搜寻公众号 Java 后端栈后盾回复“面试”,获取一份惊喜礼包。

艰深的讲,设定了延时 Level 的音讯会被暂存在名为 SCHEDULE_TOPIC_XXXX的 topic 中,并依据 level 存入特定的queuequeueId = delayTimeLevel – 1,即一个 queue 只存雷同延时的音讯,保障具备雷同发送延时的音讯可能程序生产。broker 会调度地生产SCHEDULE_TOPIC_XXXX,将音讯写入实在的 topic。

上面是整个实现计划的示意图,红色代表投递延时音讯,紫色代表定时调度到期的延时音讯:

长处:

  • Level 数固定,每个 Level 有本人的定时器,开销不大
  • 将 Level 雷同的音讯放入到同一个 Queue 中,保障了同一 Level 音讯的程序性;不同 Level 放到不同的 Queue 中,保障了投递的工夫准确性;
  • 通过只反对固定的 Level,将不同延时音讯的排序变成了固定 Level Topic 的追加写操作

毛病:

  • Level 配置的批改代价太大,固定 Level 不灵便
  • CommitLog 会因为延时音讯的存在变得很大

Pulsar

Pulsar 反对“任意工夫”的延时音讯,但实现形式和 RocketMQ 不同。

艰深的讲,Pulsar 的延时音讯会间接进入到客户端发送指定的 Topic 中,而后在堆外内存中创立一个基于工夫的优先级队列,来保护延时音讯的索引信息。延时工夫最短的会放在头上,工夫越长越靠后。在进行生产逻辑时候,再判断是否有到期须要投递的音讯,如果有就从队列外面拿出,依据延时音讯的索引查问到对应的音讯进行生产。

如果节点解体,在这个 broker 节点上的 Topics 会转移到其余可用的 broker 上,下面提到的这个优先级队列也会被重建。

上面是 Pulsar 公众号中对于 Pulsar 延时音讯的示意图。

乍一看会感觉这个计划其实非常简单,还能反对任意工夫的音讯。然而这个计划有几个比拟大的问题:

  • 内存开销:保护延时音讯索引的队列是放在堆外内存中的,并且这个队列是以订阅组(Kafka 中的生产组)为维度的,比方你这个 Topic 有 N 个订阅组,那么如果你这个 Topic 应用了延时音讯,就会创立 N 个 队列;并且随着延时音讯的增多,时间跨度的减少,每个队列的内存占用也会回升。(是的,在这个计划下,反对任意的延时音讯反而有可能让这个缺点更重大)
  • 故障转移之后延时音讯索引队列的重建工夫开销:对于跨度工夫长的大规模延时音讯,重建工夫可能会到小时级别。(摘自 Pulsar 官网公众号文章)
  • 存储开销:延时音讯的时间跨度会影响到 Pulsar 中曾经生产的音讯数据的空间回收。打个比方,你的 Topic 如果业务上要求反对一个月跨度的延时音讯,而后你发了一个延时一个月的音讯,那么你这个 Topic 中底层的存储就会保留整整一个月的音讯数据,即便这一个月中 99% 的失常音讯都曾经生产了。

对于后面第一点和第二点的问题,社区也设计了解决方案,在队列中退出工夫分区,Broker 只加载以后较近的工夫片的队列到内存,其余工夫片分区长久化磁盘,示例图如下图所示:

然而目前,这个计划并没有对应的实现版本。能够在理论应用时,规定只能应用较小时间跨度的延时音讯,来缩小前两点缺点的影响。

另外,因为内存中存的并不是延时音讯的全量数据,只是索引,所以可能要积压上百万条延时音讯才可能对内存造成显著影响,从这个角度来看,官网临时没有欠缺前两个问题也能够了解了。

至于第三个问题,预计是比拟难解决的,须要在数据存储层将延时音讯和失常音讯辨别开来,独自存储延时音讯。

QMQ

QMQ 提供任意工夫的延时 / 定时音讯,你能够指定音讯在将来两年内 (可配置) 任意工夫内投递。

把 QMQ 放到最初,是因为我感觉 QMQ 是目前开源 MQ 中延时音讯设计最正当的。外面设计的外围简略来说就是 多级工夫轮 + 延时加载 + 延时音讯独自磁盘存储。

QMQ 的延时 / 定时音讯应用的是两层 hash wheel 来实现的。

第一层位于磁盘上,每个小时为一个刻度 (默认为一个小时一个刻度,能够依据理论状况在配置里进行调整),每个刻度会生成一个日志文件(schedule log),因为 QMQ 反对两年内的延时音讯(默认反对两年内,能够进行配置批改),则最多会生成2 * 366 * 24 = 17568 个文件(如果须要反对的最大延时工夫更短,则生成的文件更少)。

第二层在内存中,当音讯的投递工夫行将到来的时候,会将这个小时的音讯索引 (索引包含音讯在 schedule log 中的 offset 和 size) 从磁盘文件加载到内存中的 hash wheel 上,内存中的 hash wheel 则是以 500ms 为一个刻度。

总结一下设计上的亮点:

  • 工夫轮算法适宜延时 / 定时音讯的场景,省去延时音讯的排序,插入删除操作都是 O(1) 的工夫复杂度;
  • 通过多级工夫轮设计,反对了超大时间跨度的延时音讯;
  • 通过延时加载,内存中只会有最近要生产的音讯,更久的延时音讯会被存储在磁盘中,对内存敌对;
  • 延时音讯独自存储(schedule log),不会影响到失常音讯的空间回收;

本文汇总了目前业界常见的延时音讯计划,并且探讨了各个计划的优缺点。心愿对读者有所启发。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0