单机任务
Java 实现
Timer是 Java 早期提供的一个简单的定时任务工具,它内部使用单线程来执行任务,任务会按照顺序依次执行。ScheduledExecutorService是 Java 5 引入的一个基于线程池的定时任务执行器,它可以并发地执行多个定时任务。DelayQueue是 Java 并发包(java.util.concurrent)中提供的一个无界阻塞队列,用于实现延时任务。- Spring 框架提供了
@Scheduled注解,用于简化定时任务的开发。它基于ScheduledExecutorService实现,支持多种调度方式,包括固定延迟、固定速率和 Cron 表达式。
Python 实现
time.sleep()函数可以让当前线程暂停执行指定的秒数,通过它可以简单实现延时效果。sched模块提供了一个通用的事件调度器,它可以根据时间安排来执行任务。你可以向调度器中添加事件,每个事件包含一个延迟时间、优先级和要执行的函数,调度器会按照设定的时间依次执行这些事件。APScheduler是一个 Python 定时任务调度库,它支持多种调度方式,包括基于日期、固定时间间隔、Cron 表达式等。该库内部通过不同的调度器(如BlockingScheduler、BackgroundScheduler等)来管理任务的执行,并且可以将任务存储在不同的后端(如内存、数据库等)。schedule是一个简单易用的 Python 定时任务库,它提供了直观的 API 来定义任务的执行时间。通过调用schedule.every()方法可以设置任务的执行间隔,然后使用.do()方法指定要执行的函数。Celery是一个分布式任务队列系统,它可以处理大量的异步任务和定时任务。它通过消息队列(如 RabbitMQ、Redis 等)来传递任务,将任务的生产者(调用任务的代码)和消费者(执行任务的工作进程)分离。在实现定时任务时,可以使用Celery Beat组件,它是一个调度器,负责按照配置的时间间隔或 Cron 表达式来触发任务。
时间轮算法
时间轮(Time Wheel)是一种高效的定时任务调度算法,用于管理和执行大量的定时任务,在许多系统和框架中被广泛应用,如 Kafka、Netty 等。
时间轮可以类比为一个时钟,它由多个槽组成,每个槽可以存放多个定时任务。时间轮以固定的时间间隔向前转动,每次转动会经过一个槽,当时间轮转动到某个槽时,就会执行该槽中所有到期的定时任务。在实现上,使用环形队列/数组作为时间片,环形队列的每个格子里维护一个任务列表。
比如,有一个 10 个 slot 的时间轮,每隔 1s 转动到下一个 slot。要建立一个 3s 后运行的任务,就放到第三个 slot 中。
为了处理更长延迟时间的任务,通常会使用多层时间轮。多层时间轮由多个不同粒度的时间轮组成,类似于现实中的时钟,有秒针、分针和时针。
当一个任务的延迟时间超过了当前时间轮的周期时,将其放入上一层时间轮中。例如,第一层时间轮的周期是 10 秒,第二层时间轮的周期是 100 秒。如果一个任务的延迟时间是 50 秒,它将被放入第二层时间轮中。当第二层时间轮的某个槽转动到当前位置时,会将该槽中的任务重新分配到下一层时间轮中,直到任务被分配到最底层的时间轮并最终执行。
- 优点
- 高效性:时间轮的时间复杂度为 O (1),添加和删除任务的操作非常快速,适合处理大量的定时任务。
- 低延迟:任务可以在接近到期时间时被快速执行,减少了任务的等待时间。
- 可扩展性:可以通过增加槽的数量或使用多层时间轮来处理不同延迟范围的任务。
- 缺点
- 精度问题:时间轮的精度取决于时间间隔(tickDuration),如果时间间隔设置得过大,可能会导致任务执行的精度不够。
- 任务丢失风险:在某些情况下,如系统崩溃或时间轮线程异常退出,可能会导致部分任务丢失。因此可能需要一套故障恢复、持久化等机制等成熟系统。
延时任务
消息队列的延迟队列
- RabbitMQ/RocketMQ:支持定时/延时消息。定时消息和延时消息本质其实是相同的,都是服务端根据消息设置的定时时间在某一固定时刻将消息投递给消费者消费。不过,在使用 MQ 定时消息之前一定要看清楚其使用限制 RocketMQ 定时时长最大值默认为 24 小时且不支持自定义修改、只支持 18 个 Level 的延时并不支持任意时间。
- Kafka 的时间轮(Kafka Streams):Kafka 本身没有直接的延迟队列功能,但可以通过 Kafka Streams 中的时间轮机制来实现类似的延时任务。时间轮是一种数据结构,用于管理定时任务,通过在时间轮中设置定时器,实现消息的延迟处理。
数据库定时轮询
- 使用定时任务结合 DB 标记:可以通过定时任务每隔一定时间查询 DB 中标记为需要执行延时任务的记录,检查任务是否达到执行时间。如果达到执行时间,则执行相应的任务逻辑,并更新任务状态。
- 为了避免 DB 主库压力大,扫描从库。
- 扫描 DB 需要时间,没有办法准确的按时执行任务,并且主从也会有延迟。
Redis 实现延时队列
- 利用 Redis 有序集合实现延时任务。有序集合的每个成员都有一个分数(score),可以将任务的执行时间作为分数,任务的唯一标识作为成员。通过不断地检查有序集合中分数最小(即最早需要执行的任务)的成员,如果其分数小于等于当前时间,就取出该任务并执行。
- 利用 Redis 过期监听机制实现延时任务。Redis 可以监听键的过期事件,当一个键过期时,会触发相应的事件。可以将任务的唯一标识作为键,任务的执行时间作为过期时间,当键过期时,就表示任务需要执行。
具体参考 Redis 延时队列。
分布式任务调度框架
如果我们需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。
通常情况下,一个分布式定时任务的执行往往涉及到下面这些角色:
- 任务:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。
- 调度器:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。
- 执行器:最后就是执行器,执行器接收调度器分派的任务并执行。
Quartz
一个很火的开源任务调度框架,完全由 Java 写成。Quartz 可以说是 Java 定时任务领域的老大哥或者说参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,比如当当网的 elastic-job 就是基于 Quartz 二次开发之后的分布式调度解决方案。
使用 Quartz 可以很方便地与 Spring 集成,并且支持动态添加任务和集群。但是,Quartz 使用起来也比较麻烦,API 繁琐。
并且,Quartz 并没有内置 UI 管理控制台,不过你可以使用 quartzui 这个开源项目来解决这个问题。
另外,Quartz 虽然也支持分布式任务。但是,它是在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。有点伪分布式的味道。
优缺点总结:
- 优点:可以与 Spring 集成,并且支持动态添加任务和集群。
- 缺点:分布式支持不友好,不支持任务可视化管理、使用麻烦(相比于其他同类型框架来说)
Elastic-Job
ElasticJob 当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。
ElasticJob-Lite 和 ElasticJob-Cloud 两者的对比如下:
| ElasticJob-Lite | ElasticJob-Cloud | |
|---|---|---|
| 无中心化 | 是 | 否 |
| 资源分配 | 不支持 | 支持 |
| 作业模式 | 常驻 | 常驻 + 瞬时 |
| 部署依赖 | ZooKeeper | ZooKeeper + Mesos |
ElasticJob 支持任务在分布式场景下的分片和高可用、任务可视化管理等功能。
从上图可以看出,Elastic-Job 没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。
Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。
@Component
@ElasticJobConf(name = "dayJob", cron = "0/10 * * * * ?", shardingTotalCount = 2,
shardingItemParameters = "0=AAAA,1=BBBB", description = "简单任务", failover = true)
public class TestJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
log.info("TestJob任务名:【{}】, 片数:【{}】, param=【{}】", shardingContext.getJobName(), shardingContext.getShardingTotalCount(),
shardingContext.getShardingParameter());
}
}相关地址:
- GitHub 地址:https://github.com/apache/shardingsphere-elasticjob。
- 官方网站:https://shardingsphere.apache.org/elasticjob/index_zh.html 。
优缺点总结:
- 优点:可以与 Spring 集成、支持分布式、支持集群、性能不错、支持任务可视化管理
- 缺点:依赖了额外的中间件比如 Zookeeper(复杂度增加,可靠性降低、维护成本变高)
XXL-JOB
XXL-JOB 于 2015 年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能,
根据 XXL-JOB 官网介绍,其解决了很多 Quartz 的不足。
Quartz 作为开源作业调度中的佼佼者,是作业调度的首选。但是集群环境中 Quartz 采用 API 的方式对任务进行管理,从而可以避免上述问题,但是同样存在以下问题:
- 问题一:调用 API 的的方式操作任务,不人性化;
- 问题二:需要持久化业务 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
- 问题三:调度逻辑和 QuartzJobBean 耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务;
- 问题四:quartz 底层以“抢占式”获取 DB 锁并由抢占成功节点负责运行任务,会导致节点负载悬殊非常大;而 XXL-JOB 通过执行器实现“协同分配式”运行任务,充分发挥集群优势,负载各节点均衡。
XXL-JOB 弥补了 quartz 的上述不足之处。
从上图可以看出,XXL-JOB 由 调度中心 和 执行器 两大部分组成。调度中心主要负责任务管理、执行器管理以及日志管理。执行器主要是接收调度信号并处理。另外,调度中心进行任务调度时,是通过自研 RPC 来实现的。
不同于 Elastic-Job 的去中心化设计, XXL-JOB 的这种设计也被称为中心化设计(调度中心调度多个执行器执行任务)。
和 Quzrtz 类似 XXL-JOB 也是基于数据库锁调度任务,存在性能瓶颈。不过,一般在任务量不是特别大的情况下,没有什么影响的,可以满足绝大部分公司的要求。
不要被 XXL-JOB 的架构图给吓着了,实际上,我们要用 XXL-JOB 的话,只需要重写 IJobHandler 自定义任务执行逻辑就可以了,非常易用!
@JobHandler(value="myApiJobHandler")
@Component
public class MyApiJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}
}还可以直接基于注解定义任务。
@XxlJob("myAnnotationJobHandler")
public ReturnT<String> myAnnotationJobHandler(String param) throws Exception {
//......
return ReturnT.SUCCESS;
}相关地址:
- GitHub 地址:https://github.com/xuxueli/xxl-job/。
- 官方介绍:https://www.xuxueli.com/xxl-job/ 。
优缺点总结:
- 优点:开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。
- 缺点:不支持动态添加任务(如果一定想要动态创建任务也是支持的,参见:xxl-job issue277)。
PowerJob
非常值得关注的一个分布式任务调度框架,分布式任务调度领域的新星。目前,已经有很多公司接入比如 OPPO、京东、中通、思科。
这个框架的诞生也挺有意思的,PowerJob 的作者当时在阿里巴巴实习过,阿里巴巴那会使用的是内部自研的 SchedulerX(阿里云付费产品)。实习期满之后,PowerJob 的作者离开了阿里巴巴。想着说自研一个 SchedulerX,防止哪天 SchedulerX 满足不了需求,于是 PowerJob 就诞生了。
更多关于 PowerJob 的故事,小伙伴们可以去看看 PowerJob 作者的视频 《我和我的任务调度中间件》。简单点概括就是:“游戏没啥意思了,我要扛起了新一代分布式任务调度与计算框架的大旗!”。
由于 SchedulerX 属于人民币产品,我这里就不过多介绍。PowerJob 官方也对比过其和 QuartZ、XXL-JOB 以及 SchedulerX。
| QuartZ | xxl-job | SchedulerX 2.0 | PowerJob | |
|---|---|---|---|---|
| 定时类型 | CRON | CRON | CRON、固定频率、固定延迟、OpenAPI | CRON、固定频率、固定延迟、OpenAPI |
| 任务类型 | 内置 Java | 内置 Java、GLUE Java、Shell、Python 等脚本 | 内置 Java、外置 Java(FatJar)、Shell、Python 等脚本 | 内置 Java、外置 Java(容器)、Shell、Python 等脚本 |
| 分布式计算 | 无 | 静态分片 | MapReduce 动态分片 | MapReduce 动态分片 |
| 在线任务治理 | 不支持 | 支持 | 支持 | 支持 |
| 日志白屏化 | 不支持 | 支持 | 不支持 | 支持 |
| 调度方式及性能 | 基于数据库锁,有性能瓶颈 | 基于数据库锁,有性能瓶颈 | 不详 | 无锁化设计,性能强劲无上限 |
| 报警监控 | 无 | 邮件 | 短信 | WebHook、邮件、钉钉与自定义扩展 |
| 系统依赖 | JDBC 支持的关系型数据库(MySQL、Oracle…) | MySQL | 人民币 | 任意 Spring Data Jpa 支持的关系型数据库(MySQL、Oracle…) |
| DAG 工作流 | 不支持 | 不支持 | 支持 | 支持 |