在我们日常开发过程中,任务调度始终是支撑核心业务流程的“隐形引擎”。无论是电商大促时的“库存扣减→订单生成→支付通知→物流同步”,还是数据处理平台的“日志采集→清洗→聚合→分析”,或是金融系统的“交易对账→风控校验→报表生成”,都需要任务按特定顺序、依赖关系高效执行。
传统定时任务工具(如Quartz)虽能解决“何时执行”的问题,但面对复杂业务场景时,逐渐暴露出三大痛点:
而 DAG(有向无环图) 恰好能通过“节点表示任务、边表示依赖”的模型,清晰描述任务的执行逻辑。结合Quartz的分布式调度能力,可构建一个“能描述依赖、支持并行、灵活容错”的任务调度系统。但现实中,这一目标的实现面临多重挑战:
本文将围绕这些核心问题,从“环境搭建→核心功能开发→测试验证”全程拆解,手把手实现一个支持“依赖管理、并行执行、动态容错”的分布式任务调度系统,解决传统工具无法应对的复杂场景需求。
java -version,输出类似 java version "1.8.0_301" 表示成功。打开浏览器,访问 https://start.spring.io/,按以下步骤创建项目:
| 步骤 | 操作 |
|---|---|
| Project | 选择 Maven Project(Java项目管理工具) |
| Language | 选择 Java |
| Spring Boot | 选择 3.2.0(最新稳定版) |
| Group | 自定义(如 com.example) |
| Artifact | 自定义(如 dag-scheduler-demo) |
| Dependencies | 添加以下依赖(点击“Add Dependencies”搜索): |
| - Spring Web(提供HTTP接口) | |
| - Spring Data Redis(操作Redis) | |
| - Quartz Scheduler(分布式任务调度核心) | |
| - Lombok(简化Java代码) | |
| - Fastjson(解析JSON格式的DAG定义) |
pom.xml 文件)。登录MySQL,创建存储Quartz元数据的数据库:
在 src/main/resources/application.yml 中添加以下配置:
首次启动Spring Boot应用时,Quartz会自动在 quartz_db 数据库中创建所需的表(如 QRTZ_TRIGGERS、QRTZ_CRON_TRIGGERS 等)。启动后,登录MySQL验证:
DAG(有向无环图)用JSON格式描述任务关系,包含节点(任务)和边(依赖关系)。例如,一个“订单支付后通知”的DAG定义如下:
关键说明:
dagId:DAG的唯一标识(如 order_pay_notify),用于区分不同的任务链路;nodes:任务列表,每个任务有 id(唯一ID)、name(显示名称)、jobClass(任务类的全限定名);edges:依赖关系,from 是前置任务ID,to 是后续任务ID(表示“前置任务完成后才能执行后续任务”)。需要将JSON格式的DAG定义转换为Java对象,方便程序处理。
在 src/main/java/com/example/dag/entity 目录下创建以下类:
使用Fastjson将JSON字符串转换为 DagDefinition 对象:
DAG的任务必须按依赖关系执行(比如“支付校验”完成后才能执行“发送短信”)。我们需要用 拓扑排序 算法(Kahn算法)生成执行顺序。
拓扑排序的核心是:
如果有任务未被执行,说明DAG中存在循环依赖(比如A依赖B,B依赖A),此时拓扑排序会失败。
在 src/main/java/com/example/dag/service 目录下创建 TopologySorter.java:
Quartz是Java生态中最经典的任务调度框架,支持定时触发和分布式执行。我们需要让Quartz根据拓扑排序的结果,按顺序触发任务。
所有任务需要继承Quartz的 QuartzJobBean 类,并实现 executeInternal 方法(任务的具体逻辑在这里编写)。
在 src/main/java/com/example/dag/job 目录下创建 BaseJob.java:
编写一个具体的任务类(例如“发送短信”任务),继承 BaseJob 并实现 executeTask 方法:
创建一个调度器类,负责解析DAG、生成执行顺序,并调用Quartz触发每个任务:
为了知道任务是否成功或失败,需要记录每个任务的执行状态。这里用Redis存储状态(方便分布式访问)。
在 src/main/java/com/example/dag/enum 目录下创建 TaskState.java:
使用Redis存储每个任务的状态,并提供更新状态的方法:
任务执行失败时,需要自动重试或触发熔断机制,避免无限失败。
定义一个简单的指数退避重试策略(失败后等待时间逐渐增加):
当某个任务频繁失败(比如失败率超过50%),触发熔断,暂时跳过该任务:
mvn spring-boot:run,启动应用;Started DagSchedulerDemoApplication in X seconds 表示启动成功。通过HTTP接口触发DAG执行(需要先添加一个Controller):
使用Postman或curl发送以下请求(替换 dagJson 为你的DAG定义):
任务执行时,控制台会打印每个任务的触发信息(如“触发任务:check_pay”)。
使用Redis客户端(如 redis-cli)查看任务状态:
现象:启动应用时报错 org.quartz.JobPersistenceException: Couldn't store trigger...。
原因:Quartz的元数据表未正确创建。
解决:手动执行Quartz的建表脚本(https://github.com/quartz-scheduler/quartz/tree/main/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore),或检查MySQL用户是否有建表权限。
现象:“发送短信”任务在“支付校验”完成前就执行了。
原因:拓扑排序逻辑错误,或Quartz触发任务时未等待依赖。
解决:检查 TopologySorter 的排序逻辑,确保入度计算正确;或在触发任务时,检查前置任务是否已完成(通过 StateManager 查询状态)。
现象:任务执行失败后,状态直接变为 FAIL,没有重试。
原因:未在任务中调用重试逻辑,或 RetryPolicy 未正确配置。
解决:在 BaseJob 的 executeInternal 方法中,捕获异常后调用重试逻辑(如使用 RetryPolicy 计算间隔,重新触发任务)。
到此为止,已经完成了一个支持“任务依赖、并行执行、失败重试”的分布式任务调度系统的搭建。核心流程是:
当然,这只是一个基础实现,实际生产环境中还可以扩展:
最后,希望通过本文,你能大致了解DAG+Quartz整个流程,后续可以在此基础上进一步优化探索!
原文作者:臭技术佬;如有侵权,请联系管理员删除。