分布式定时任务 Quartz 全解析:从底层原理到 Spring Boot 实战
-
-
- [分布式定时任务 Quartz 全解析:从底层原理到 Spring Boot 实战](#分布式定时任务 Quartz 全解析:从底层原理到 Spring Boot 实战)
- [一、Quartz 底层原理:分布式协调机制](#一、Quartz 底层原理:分布式协调机制)
-
- [1. 核心架构:以数据库为中心的集群协同](#1. 核心架构:以数据库为中心的集群协同)
- [2. 关键数据库表(QRTZ_*)](#2. 关键数据库表(QRTZ_*))
- [3. 任务执行唯一性保障流程](#3. 任务执行唯一性保障流程)
- [4. 核心注解:`@DisallowConcurrentExecution`](#4. 核心注解:
@DisallowConcurrentExecution)
- [二、Quartz 核心组件与 Spring Boot 集成要点](#二、Quartz 核心组件与 Spring Boot 集成要点)
-
- [1. 核心组件](#1. 核心组件)
- [2. Spring Boot 集成核心配置](#2. Spring Boot 集成核心配置)
- [三、Spring Boot 集成 Quartz 集群:完整实战案例](#三、Spring Boot 集成 Quartz 集群:完整实战案例)
-
- [1. 环境准备](#1. 环境准备)
- [2. 完整代码实现](#2. 完整代码实现)
-
- (1)依赖引入(pom.xml)
- (2)配置文件(application.yml)
- [(3)初始化 Quartz 数据库表](#(3)初始化 Quartz 数据库表)
- [(4)定义 Job 类(任务逻辑)](#(4)定义 Job 类(任务逻辑))
- [(5)配置 JobDetail 和 Trigger(注册任务到集群)](#(5)配置 JobDetail 和 Trigger(注册任务到集群))
- [(6)启动类(Spring Boot 入口)](#(6)启动类(Spring Boot 入口))
- 四、验证与注意事项
-
- [1. 验证集群效果](#1. 验证集群效果)
- [2. 关键注意事项](#2. 关键注意事项)
- 五、总结
-
分布式定时任务 Quartz 全解析:从底层原理到 Spring Boot 实战
Quartz 是一款功能强大的开源分布式定时任务调度框架 ,支持复杂的调度策略(如 Cron 表达式、日历排除)、集群部署、故障转移和动态任务管理。其核心优势在于基于数据库的分布式协调机制 ,无需依赖 ZooKeeper/Redis 等中间件,即可实现"集群中同一任务仅一个实例执行"。本文将从底层原理 、核心组件 、Spring Boot 集成实战三部分展开,并提供完整可复用的代码示例。
一、Quartz 底层原理:分布式协调机制
1. 核心架构:以数据库为中心的集群协同
Quartz 集群中所有节点(服务实例)通过共享关系型数据库 (如 MySQL)实现通信,无中心节点。核心设计思想:"数据库锁 + 状态标记 + 心跳同步",确保任务调度的唯一性和容错性。
2. 关键数据库表(QRTZ_*)
Quartz 依赖以下核心表实现集群协调(以 MySQL 为例):
| 表名 | 作用 |
|---|---|
QRTZ_JOB_DETAILS |
存储任务(Job)元数据(类名、描述、是否持久化) |
QRTZ_TRIGGERS |
存储触发器(Trigger)信息(触发时间、状态、关联 Job) |
QRTZ_CRON_TRIGGERS |
存储 Cron 触发器表达式(如 0 0/5 * * * ?) |
QRTZ_LOCKS |
分布式锁表 :存储集群级锁(如 TRIGGER_ACCESS),用于节点互斥竞争 |
QRTZ_FIRED_TRIGGERS |
记录"已触发未执行完"的触发器(标记执行节点、状态:ACQUIRED/EXECUTING) |
QRTZ_SCHEDULER_STATE |
记录集群节点状态(节点名、最后心跳时间),检测节点存活 |
3. 任务执行唯一性保障流程
当任务触发时间到达时,集群节点通过以下步骤竞争执行权:
-
竞争分布式锁 :
节点通过
SELECT * FROM QRTZ_LOCKS WHERE LOCK_NAME = 'TRIGGER_ACCESS' FOR UPDATE语句竞争行锁,确保同一时刻只有一个节点能操作触发器。 -
标记任务状态 :
获取锁的节点查询
QRTZ_TRIGGERS,若触发器状态为WAITING(等待执行),则更新为ACQUIRED(已获取),并在QRTZ_FIRED_TRIGGERS记录执行节点信息。 -
执行任务 :
节点将触发器状态更新为
EXECUTING(执行中),执行 Job 逻辑。其他节点查询QRTZ_FIRED_TRIGGERS发现状态为EXECUTING,则放弃执行。 -
释放锁与状态更新 :
任务执行成功/失败后,节点更新触发器状态(如
COMPLETE/ERROR),删除QRTZ_FIRED_TRIGGERS记录,释放锁。 -
心跳与故障转移 :
节点定期(默认 30 秒)向
QRTZ_SCHEDULER_STATE更新心跳。若某节点心跳超时(如 30 秒未更新),其他节点会清理其遗留的"僵尸任务"并重新触发。
4. 核心注解:@DisallowConcurrentExecution
在 Job 类上添加该注解,可防止单节点内同一任务并发执行(如任务执行时间超过触发间隔)。集群中"单实例执行"仍依赖数据库锁,该注解是补充机制。
二、Quartz 核心组件与 Spring Boot 集成要点
1. 核心组件
- Job :任务接口,定义具体执行逻辑(实现
org.quartz.Job接口)。 - JobDetail :任务元数据(Job 类、参数、描述),通过
JobBuilder构建。 - Trigger :触发器,定义任务执行规则(如 Cron 表达式、间隔时间),通过
TriggerBuilder构建。 - Scheduler :调度器,管理 JobDetail 和 Trigger,通过
StdSchedulerFactory或 Spring 自动配置。
2. Spring Boot 集成核心配置
- 依赖 :
spring-boot-starter-quartz(自动集成 Quartz 与 Spring 上下文)。 - JobStore :集群模式必须用
JobStoreTX(基于 JDBC 事务),通过org.quartz.jobStore.isClustered=true启用集群。 - 数据源:所有节点共享同一数据库(需支持行锁,如 MySQL InnoDB)。
- 实例 ID :每个节点需唯一(
org.quartz.scheduler.instanceId=AUTO自动生成)。
三、Spring Boot 集成 Quartz 集群:完整实战案例
1. 环境准备
- 数据库 :MySQL 8.0+(创建数据库
quartz_cluster,字符集utf8mb4)。 - 依赖:Spring Boot 2.7.x + Quartz 2.3.x(版本兼容见 https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明)。
2. 完整代码实现
(1)依赖引入(pom.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version> <!-- Spring Boot 2.7.x 稳定版 -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>quartz-cluster-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>quartz-cluster-demo</name>
<dependencies>
<!-- Spring Boot Web(基础依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Quartz 核心依赖(Spring Boot 自动配置) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 连接池(HikariCP,Spring Boot 默认) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)配置文件(application.yml)
yaml
spring:
# 数据源配置(所有集群节点共享此数据库)
datasource:
url: jdbc:mysql://localhost:3306/quartz_cluster?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10 # 连接池大小(根据集群节点数调整)
# Quartz 配置(集群模式核心)
quartz:
job-store-type: jdbc # 使用 JDBC 存储(集群必须用 JDBC)
jdbc:
initialize-schema: always # 自动初始化 Quartz 数据库表(首次启动时创建 QRTZ_* 表)
properties:
org:
quartz:
scheduler:
instanceName: ClusterScheduler # 调度器实例名(集群中唯一标识)
instanceId: AUTO # 实例 ID(自动生成,确保每个节点唯一)
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX # 事务型 JobStore(集群推荐)
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # MySQL 方言
tablePrefix: QRTZ_ # 数据库表前缀(需与初始化表名一致)
isClustered: true # 启用集群模式(核心开关)
clusterCheckinInterval: 20000 # 节点心跳间隔(毫秒,默认 15000)
useProperties: false # 允许存储非字符串类型参数
threadPool:
class: org.quartz.simpl.SimpleThreadPool # 简单线程池
threadCount: 10 # 线程数(根据任务数量调整)
threadPriority: 5 # 线程优先级(1-10,默认 5)
dataSource: myDs # 数据源名称(需与 Spring 数据源对应)
(3)初始化 Quartz 数据库表
Quartz 集群需预先创建 QRTZ_* 表,Spring Boot 可通过 initialize-schema: always 自动执行建表 SQL(位于 org.quartz.impl.jdbcjobstore 包的 tables_mysql.sql)。若自动初始化失败,可手动执行以下 SQL(https://github.com/quartz-scheduler/quartz/blob/main/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql):
(4)定义 Job 类(任务逻辑)
java
package com.example.quartzclusterdemo.job;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.stereotype.Component;
/**
* 集群定时任务示例:模拟数据同步
* 注意点:
* 1. 添加 @DisallowConcurrentExecution 防止单节点内并发执行
* 2. 任务逻辑需幂等(重复执行不影响结果)
*/
@Slf4j
@Component
@DisallowConcurrentExecution // 单节点内禁止并发执行(集群中主要靠数据库锁)
public class DataSyncJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 从 JobDataMap 获取参数(可选)
String param = context.getJobDetail().getJobDataMap().getString("param");
log.info("===== 集群任务执行中 =====");
log.info("当前节点:{}", context.getScheduler().getSchedulerInstanceId());
log.info("任务参数:{}", param);
log.info("执行数据同步逻辑...");
// TODO: 实际业务逻辑(如数据库同步、API 调用等)
}
}
(5)配置 JobDetail 和 Trigger(注册任务到集群)
java
package com.example.quartzclusterdemo.config;
import com.example.quartzclusterdemo.job.DataSyncJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Quartz 任务配置类:定义 JobDetail 和 Trigger
*/
@Configuration
public class QuartzConfig {
/**
* 定义 JobDetail(任务元数据)
* 注意点:
* 1. withIdentity(name, group):任务唯一标识(name + group),集群中不可重复
* 2. usingJobData:传递参数到 Job(支持基本类型、String、Serializable 对象)
* 3. storeDurably:即使无触发器关联,也持久化任务(避免被自动删除)
*/
@Bean
public JobDetail dataSyncJobDetail() {
return JobBuilder.newJob(DataSyncJob.class)
.withIdentity("dataSyncJob", "dataSyncGroup") // 任务名+组名(唯一)
.usingJobData("param", "集群任务参数-2024") // 传递参数
.storeDurably() // 持久化任务
.build();
}
/**
* 定义 CronTrigger(触发器:每 5 分钟执行一次)
* 注意点:
* 1. withSchedule(CronScheduleBuilder):使用 Cron 表达式(推荐复杂调度)
* 2. forJob:关联 JobDetail
* 3. withIdentity:触发器唯一标识(name + group)
*/
@Bean
public Trigger dataSyncTrigger() {
// Cron 表达式:秒 分 时 日 月 周 年(可选),此处为"每 5 分钟的第 0 秒"
CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule("0 0/5 * * * ?");
return TriggerBuilder.newTrigger()
.forJob(dataSyncJobDetail()) // 关联 JobDetail
.withIdentity("dataSyncTrigger", "dataSyncGroup") // 触发器名+组名(唯一)
.withSchedule(cronSchedule)
.build();
}
}
(6)启动类(Spring Boot 入口)
java
package com.example.quartzclusterdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class QuartzClusterDemoApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzClusterDemoApplication.class, args);
}
}
四、验证与注意事项
1. 验证集群效果
- 启动多个节点 :复制项目,修改
server.port(如 8081、8082),分别启动两个 Spring Boot 实例。 - 观察日志 :同一任务触发周期内(如每 5 分钟),只有一个节点会打印"集群任务执行中"日志。
- 数据库验证 :查看
QRTZ_FIRED_TRIGGERS表,任务执行时会记录执行节点和状态(EXECUTING)。
2. 关键注意事项
- 数据库选择:必须用支持行锁的数据库(如 MySQL InnoDB),避免锁失效。
- 节点唯一性 :
org.quartz.scheduler.instanceId=AUTO自动生成唯一 ID,禁止手动设置为相同值。 - 时钟同步:集群节点需通过 NTP 同步系统时间,避免触发时间偏差。
- 任务幂等性:尽管 Quartz 保证单实例执行,仍需确保任务逻辑幂等(如重复执行不影响结果)。
- 事务隔离级别 :数据库隔离级别设为
REPEATABLE_READ(MySQL 默认),避免脏读导致锁竞争异常。
五、总结
Quartz 集群通过数据库锁(QRTZ_LOCKS)+ 状态标记(QRTZ_FIRED_TRIGGERS)+ 心跳同步(QRTZ_SCHEDULER_STATE) 实现分布式任务唯一执行。在 Spring Boot 中集成时,只需配置 isClustered=true、共享数据库、定义 Job/Trigger,即可快速搭建可靠的分布式定时任务系统。本文提供的完整例子可直接复现,适用于数据同步、报表生成、缓存刷新等场景。