调度框架的选型及原理

今天我们将探讨任务调度框架的选择及其背后的机制。任务调度是一个普遍需求,无论是在单机还是分布式环境中,都有调度的存在。围绕调度场景,诞生了很多的调度中间件。最简单的场景,比如单机服务器巡检,当磁盘使用超过 90%就自动去清理,这一点我们写一段 shell 就可以实现。

在更复杂的场景中,例如在一个大型电商平台的促销活动期间,可能会由于系统负载过高导致订单处理延迟。在这种情况下,需要从大量订单中识别出处理异常的订单,并及时进行处理。由于涉及的数据量巨大,因此需要采用分布式调度,将处理任务分配到多台机器上执行。下面我将从单机和分布式两个角度,聊聊调度框架的选型和原理。

单机调度

单机任务调度是一个相对简单的任务,通常可以通过 Java 中的 Timer 类实现。Timer 类利用优先队列来管理任务,任务会根据下一次执行时间进行排序。

然而,这种串行任务处理方式存在一些缺陷。例如,如果 Timer 中的某个任务执行时间过长,可能会影响后续任务的执行。此外,如果任务中出现异常,整个 Timer 可能会停止运行。因此,阿里巴巴的 Java 开发规范已明确禁止使用 Timer,而官方也推荐使用 ScheduledExecutorService 类进行单机任务调度。

你可能对以下代码非常熟悉,它在许多项目中都有应用:

ini 复制代码
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
executor.scheduleAtFixedRate(task, delay, period, TimeUnit.SECONDS);

这段代码的目的是定期更新本地内存中的数据。其工作原理是在一个固定的时间间隔内执行一个任务。由于 ScheduledExecutorService 基于线程池,因此它不会像 Timer 那样受到前一个任务执行时间的影响。

然而,即便是 ScheduledExecutorService,它的功能也是薄弱的(Java 自带的类功能都不是很强)。于是我们的 Spring 框架开始研发任务组件了,Spring-Task 应运而生。

Spring-Task 基于 ScheduledExecutorService 实现,并支持 Cron 表达式。Cron 表达式是一种独立于语言的表达方式,几乎所有编程语言都支持。Cron 表达式允许你指定任务在每天或每月的特定时间执行。如果你对调度任务感兴趣,Cron 表达式是一个必须掌握的概念。

调度的底层原理

这个调度的底层算法叫做"时间轮算法"。时间轮可以看作是一个环形队列,通常基于数组实现,数组的首尾相连。每个数组元素可以存储一个定时任务列表,类似于手表表盘的设计。

时间轮的每个刻度代表一个特定的时间间隔。这个设计和手表完全一样,也是表盘上有 12 个刻度,且每相邻的两个刻度代表一个小时的时间间隔。

下图是一个有 8 个刻度的"手表",每个刻度代表的时间间隔是 1s,且转完一圈需要 8s。假设当前时间是 0,当我们需要新建一个 3s 后执行的任务,只需要将任务放在下标为 3 的格子中即可。

当我们需要创建一个 9s 后执行的任务怎么办呢?很容易想到,就像手表一样,时间轮会进入下一次循环,于是任务会被放在下标为 1 的格子中。

注意这里与 1s 对应的格子不同,该任务是转动到第 2 轮中下标为 1 的格子。这种方式我们需要额外记录转动的轮数,也就是除了关注格子的序号,我们还需要关注转动了几轮。比如 3s 后执行的任务应该放置在第 1 轮的编号为 3 的格子,9s 后执行的任务放在第 2 轮编号为 1 的格子中。这种记录方法非常简单,我们常用的 Netty 就采用了这种方案。

除了上面这种方案,还有一种更贴近"手表"的时间轮设计。咱们的表盘上有时、分、秒 3 个区间刻度,假设你下午 19:35:00 有一个很重要的活动,想要确定是否已经到了这个时间,你就要先找到时针有没有到 19 点,接着看分针有没有到 35 分,最后看秒针是不是指向了 0。

我们可以直接使用手表作为时间轮。这样我们把时间轮分为 3 层:第一层单位是 1 秒;第二层单位分钟是 60 秒;第三层单位小时是 3600 秒。假设当前时间是 0 秒,我们添加一个 2 小时 15 分 10 秒之后执行的任务 A,它首先会在第三层运动;当 2 小时过去了,时针来到了 2 点~ 3 点之间的位置,此时任务 A 会被移动到第二层,即分钟所在的层;当分针指向了 15 的时候,任务又会被移动到秒钟所在的第一层;当秒针指向 10 的时候,任务就会被触发执行。

这种设计类似于手表的时针、分针和秒针的运动,任务在不同层之间移动的过程称为时间轮的升降级。时间轮算法适用于任务数量较多的定时任务场景,其任务写入和执行的时间复杂度都是常数。

分布式调度

聊完了单机调度的选型和原理,接下来,我们讨论市场上最常见的三个分布式调度框架:Quartz、Elastic-Job 和 XXL-Job。

Quartz

Quartz 是许多开发者接触的第一个任务调度框架。它能够与 Spring 无缝集成,并支持动态添加任务。然而,Quartz 缺乏官方的控制台界面,这使得任务管理变得复杂。GitHub 上有一些民间的开源 Quartz 控制台页面,你可以将就用一下。

Quartz 是基于数据库来实现的分布式任务。这样对代码的侵入比较严重,这也是为什么虽然 Quartz 看上去支持了分布式任务,但是我们并不推荐的原因。

Elastic-Job

Elastic-Job 是当当网开源的一个基于 Quartz 改进的分布式调度方案。它支持任务在分布式环境中的分片和高可用性,并提供了可视化控制台。Elastic-Job 使用 ZooKeeper 作为注册中心,负责协调任务分配到不同节点。它弥补了 Quartz 的不足,是一个真正的分布式调度框架。

XXL-Job

XXL-Job 自 2015 年开源以来,一直是一个优秀的轻量级分布式任务调度框架。它的名字来源于作者许雪里的首字母缩写。XXL-Job 支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能。它弥补了 Quartz 的许多不足。

然而,和 Elastic-Job 使用 ZooKeeper 来分散执行任务不同,XXL-Job 采用了中心化的设计。这样一来,Elastic-Job 在具有大量任务的场景下性能优势就很明显了,而 XXL-Job 则在这种大量的任务场景下性能不佳。并且和 Quartz 一样,Elastic-Job 也是采用了数据库来实现任务调度,同样存在性能瓶颈。因此中小规模的业务可以使用。

这里我总结了一张表,你可以通过它来看看 Quartz、Elastic-Job 和 XXL-Job 三者之间的区别和适用场景。

总结

今天,我们讨论了任务调度框架的选择和原理,包括单机调度和分布式调度。对于单机调度,我们推荐使用 Spring-Task,它封装了 ScheduledExecutorService 并支持 Cron 表达式。我们还介绍了时间轮算法的原理,它类似于手表的设计,易于理解。

最后我们对比了市面上常见的分布式调度:Quartz、Elastic-Job 和 XXL-Job。简单来说,就是尽量不要用 Quartz。如果你的任务多并且调度频繁,比如设计一个独立的任务平台,建议用 Elastic-Job,而对比较小的场景则可以用 XXL-Job。

相关推荐
余衫马3 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng6 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
七星静香7 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员8 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU9 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie612 分钟前
在IDEA中使用Git
java·git
Elaine20239127 分钟前
06 网络编程基础
java·网络
Gemini199528 分钟前
分布式和微服务的区别
分布式·微服务·架构
G丶AEOM29 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201330 分钟前
tp接口 入口文件 500 错误原因
java·开发语言