Skip to content

Quartz 任务调度框架详解

1. 概述

1.1 Quartz简介

Quartz是一个功能强大、开源的任务调度框架,用于在Java应用中执行定时任务。它支持复杂的调度表达式,可以精确控制任务的执行时间和频率。Quartz提供了持久化机制,可以在应用重启后恢复任务调度状态,并具有集群支持功能,确保任务在分布式环境中的可靠性和一致性。

1.2 Quartz的核心特性

  • 灵活的调度表达式: 支持Cron表达式,可以精确控制任务的执行时间
  • 任务持久化: 支持多种存储方式,可以将任务信息保存到数据库中
  • 集群支持: 支持分布式环境,避免任务重复执行
  • 事务支持: 支持与Spring事务集成,确保任务执行的事务一致性
  • 任务优先级: 可以设置任务的优先级,高优先级的任务先执行
  • 监听器机制: 提供多种监听器接口,可以监听任务调度的各个阶段
  • 错误处理: 提供丰富的错误处理机制,包括重试、失败通知等
  • 丰富的API: 提供简洁明了的API,便于使用和扩展

1.3 Quartz的应用场景

  • 定时数据同步: 定期从其他系统同步数据
  • 日志清理: 定期清理系统日志
  • 报表生成: 定期生成业务报表
  • 缓存更新: 定期更新缓存数据
  • 系统监控: 定期检查系统状态
  • 定时发送邮件/短信: 定期向用户发送通知
  • 数据备份: 定期备份重要数据

2. Quartz核心概念

2.1 Job (任务)

Job是一个接口,代表要执行的工作单元。开发者需要实现Job接口,并在execute方法中编写具体的业务逻辑。

java
public interface Job {
    void execute(JobExecutionContext context) throws JobExecutionException;
}

JobExecutionContext提供了任务执行的上下文信息,包含了Scheduler、Trigger、JobDetail等信息。

2.2 JobDetail (任务详情)

JobDetail是对Job的进一步封装,包含了Job的实例信息、任务名称、组名、描述等元数据,以及JobDataMap用于存储任务参数。

2.3 Trigger (触发器)

Trigger定义了何时触发任务执行。Quartz提供了多种触发器类型,如SimpleTrigger和CronTrigger等。

2.4 Scheduler (调度器)

Scheduler是Quartz的核心组件,负责管理Job和Trigger的生命周期,并协调它们的执行。

2.5 JobBuilder

JobBuilder用于构建JobDetail对象,提供了流式API来设置任务的各种属性。

2.6 TriggerBuilder

TriggerBuilder用于构建Trigger对象,提供了流式API来设置触发器的各种属性。

2.7 JobDataMap

JobDataMap是一个Map实现,用于存储任务执行所需的参数。它可以在任务执行时通过JobExecutionContext获取。

2.8 JobExecutionContext

JobExecutionContext提供了任务执行的上下文环境,包含了Scheduler、Trigger、JobDetail等信息,以及JobDataMap中的参数。

3. Quartz的依赖配置

3.1 Maven依赖

xml
<!-- 核心依赖 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

<!-- 可选,用于支持Spring集成 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.3.9</version>
</dependency>

<!-- 可选,用于支持JTA事务 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.3.2</version>
</dependency>

3.2 Gradle依赖

groovy
implementation 'org.quartz-scheduler:quartz:2.3.2'
implementation 'org.springframework:spring-context-support:5.3.9'
implementation 'org.quartz-scheduler:quartz-jobs:2.3.2'

4. Quartz的基本使用

4.1 创建一个简单的Job

java
public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取任务参数
        JobDataMap dataMap = context.getMergedJobDataMap();
        String name = dataMap.getString("name");
        
        // 输出当前时间和任务参数
        System.out.println("Hello, " + name + "! Current time: " + new Date());
    }
}

4.2 配置并启动Scheduler

java
public class QuartzDemo {
    public static void main(String[] args) throws SchedulerException {
        // 创建Scheduler
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        
        // 启动Scheduler
        scheduler.start();
        
        // 创建JobDetail
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job1", "group1")
                .usingJobData("name", "Quartz") // 设置任务参数
                .build();
        
        // 创建Trigger (每5秒执行一次)
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .startNow() // 立即开始
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(5) // 每5秒执行一次
                        .repeatForever()) // 无限重复
                .build();
        
        // 注册Job和Trigger
        scheduler.scheduleJob(job, trigger);
        
        // 运行一段时间后关闭Scheduler
        try {
            Thread.sleep(60000); // 等待60秒
            scheduler.shutdown(); // 关闭Scheduler
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5. Quartz的高级特性

5.1 Cron表达式

Cron表达式是Quartz中最强大的调度表达式,用于定义复杂的定时规则。Cron表达式由6个或7个字段组成,分别表示秒、分、时、日、月、周、年(可选)。

┌───────────── 秒 (0-59)
│ ┌───────────── 分 (0-59)
│ │ ┌───────────── 时 (0-23)
│ │ │ ┌───────────── 日 (1-31)
│ │ │ │ ┌───────────── 月 (1-12 或 JAN-DEC)
│ │ │ │ │ ┌───────────── 周 (0-7 或 SUN-SAT, 0和7都表示周日)
│ │ │ │ │ │ ┌───────────── 年 (可选,1970-2099)
│ │ │ │ │ │ │
* * * * * * *

常用Cron表达式示例:

  • 0 0 12 * * ? - 每天中午12点执行
  • 0 15 10 ? * * - 每天上午10:15执行
  • 0 15 10 * * ? * - 每天上午10:15执行
  • 0 15 10 * * ? 2024 - 2024年每天上午10:15执行
  • 0 * 14 * * ? - 每天下午2点到2:59之间每分钟执行一次
  • 0 0/5 14 * * ? - 每天下午2点到2:55之间每5分钟执行一次
  • 0 0/5 14,18 * * ? - 每天下午2点到2:55和6点到6:55之间每5分钟执行一次
  • 0 0-5 14 * * ? - 每天下午2点到2:05之间每分钟执行一次
  • 0 10,44 14 ? 3 WED - 每年3月的每个星期三下午2:10和2:44执行
  • 0 15 10 ? * MON-FRI - 每周一至周五上午10:15执行
  • 0 15 10 15 * ? - 每月15日上午10:15执行
  • 0 15 10 L * ? - 每月最后一天上午10:15执行
  • 0 15 10 ? * 6L - 每月最后一个星期五上午10:15执行
  • 0 15 10 ? * 6L 2020-2024 - 2020年至2024年每月最后一个星期五上午10:15执行
  • 0 15 10 ? * 6#3 - 每月第三个星期五上午10:15执行

5.2 使用CronTrigger

java
// 创建CronTrigger (每天上午10点执行)
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger2", "group1")
        .withSchedule(CronScheduleBuilder.cronSchedule("0 0 10 * * ?"))
        .build();

// 注册Job和Trigger
scheduler.scheduleJob(job, trigger);

5.3 任务持久化

Quartz支持多种持久化方式,包括内存存储和数据库存储。数据库存储可以确保在应用重启后恢复任务调度状态。

配置数据库存储

创建quartz.properties文件:

properties
# 配置调度器实例名称
org.quartz.scheduler.instanceName = MyScheduler

# 配置调度器实例ID,在集群环境中需要唯一
org.quartz.scheduler.instanceId = AUTO

# 配置线程池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5

# 配置作业存储方式
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

# 配置驱动委托类
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

# 配置数据源
org.quartz.jobStore.dataSource = myDS

# 配置表前缀
org.quartz.jobStore.tablePrefix = QRTZ_

# 配置数据库表是否使用触发器类型字段
org.quartz.jobStore.useProperties = false

# 配置集群
org.quartz.jobStore.isClustered = false

# 配置数据源
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = root
org.quartz.dataSource.myDS.maxConnections = 10

使用自定义配置创建Scheduler:

java
// 加载自定义配置
Properties properties = new Properties();
properties.load(new FileInputStream("quartz.properties"));

// 使用自定义配置创建SchedulerFactory
SchedulerFactory schedulerFactory = new StdSchedulerFactory(properties);

// 创建Scheduler
Scheduler scheduler = schedulerFactory.getScheduler();

5.4 任务优先级

当有多个任务同时触发时,可以通过设置任务的优先级来决定任务的执行顺序,优先级高的任务先执行。

java
// 创建JobDetail时设置优先级
JobDetail job = JobBuilder.newJob(PriorityJob.class)
        .withIdentity("priorityJob", "group1")
        .withPriority(10) // 设置优先级为10
        .build();

// 创建Trigger时设置优先级
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("priorityTrigger", "group1")
        .withPriority(5) // 设置优先级为5
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5)
                .repeatForever())
        .build();

5.5 任务监听器

Quartz提供了多种监听器接口,可以监听任务调度的各个阶段:

  1. JobListener - 监听Job的执行
  2. TriggerListener - 监听Trigger的触发
  3. SchedulerListener - 监听Scheduler的事件

实现JobListener

java
public class MyJobListener implements JobListener {
    @Override
    public String getName() {
        return "MyJobListener";
    }
    
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        // 任务即将执行前调用
        System.out.println("Job is about to be executed: " + context.getJobDetail().getKey());
    }
    
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        // 任务执行被否决时调用
        System.out.println("Job execution vetoed: " + context.getJobDetail().getKey());
    }
    
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        // 任务执行完成后调用
        System.out.println("Job was executed: " + context.getJobDetail().getKey());
        if (jobException != null) {
            System.out.println("Job execution failed: " + jobException.getMessage());
        }
    }
}

注册JobListener:

java
// 创建并注册JobListener
scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(JobKey.jobKey("job1", "group1")));

// 或注册到所有Job
scheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());

6. Quartz与Spring集成

6.1 Spring配置文件

xml
<!-- 配置JobDetail -->
<bean id="helloJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass" value="com.example.HelloJob" />
    <property name="group" value="group1" />
    <property name="name" value="job1" />
    <property name="durability" value="true" />
    <property name="jobDataAsMap">
        <map>
            <entry key="name" value="Spring Quartz" />
        </map>
    </property>
</bean>

<!-- 配置SimpleTrigger -->
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
    <property name="jobDetail" ref="helloJob" />
    <property name="startDelay" value="0" />
    <property name="repeatInterval" value="5000" />
    <property name="repeatCount" value="-1" />
</bean>

<!-- 配置CronTrigger -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
    <property name="jobDetail" ref="helloJob" />
    <property name="cronExpression" value="0 0 10 * * ?" />
</bean>

<!-- 配置SchedulerFactory -->
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="transactionManager" ref="transactionManager" />
    <property name="quartzProperties">
        <props>
            <prop key="org.quartz.jobStore.isClustered">true</prop>
            <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
        </props>
    </property>
    <property name="triggers">
        <list>
            <ref bean="simpleTrigger" />
            <ref bean="cronTrigger" />
        </list>
    </property>
</bean>

6.2 使用Java配置

java
@Configuration
public class QuartzConfig {
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    // 配置JobDetail
    @Bean
    public JobDetailFactoryBean helloJob() {
        JobDetailFactoryBean factory = new JobDetailFactoryBean();
        factory.setJobClass(HelloJob.class);
        factory.setGroup("group1");
        factory.setName("job1");
        factory.setDurability(true);
        
        Map<String, Object> jobDataMap = new HashMap<>();
        jobDataMap.put("name", "Spring Quartz");
        factory.setJobDataAsMap(jobDataMap);
        
        return factory;
    }
    
    // 配置SimpleTrigger
    @Bean
    public SimpleTriggerFactoryBean simpleTrigger() {
        SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean();
        factory.setJobDetail(helloJob().getObject());
        factory.setStartDelay(0);
        factory.setRepeatInterval(5000);
        factory.setRepeatCount(-1); // 无限重复
        
        return factory;
    }
    
    // 配置CronTrigger
    @Bean
    public CronTriggerFactoryBean cronTrigger() {
        CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
        factory.setJobDetail(helloJob().getObject());
        factory.setCronExpression("0 0 10 * * ?"); // 每天上午10点执行
        
        return factory;
    }
    
    // 配置SchedulerFactory
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setTransactionManager(transactionManager);
        
        Properties properties = new Properties();
        properties.setProperty("org.quartz.jobStore.isClustered", "true");
        properties.setProperty("org.quartz.scheduler.instanceId", "AUTO");
        factory.setQuartzProperties(properties);
        
        factory.setTriggers(simpleTrigger().getObject(), cronTrigger().getObject());
        
        return factory;
    }
}

6.3 Spring Boot集成

Spring Boot提供了对Quartz的自动配置支持,使用起来更加简便。

依赖配置

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

<!-- 如果需要持久化,添加数据库依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

配置文件

yaml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/quartz
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  
  # Quartz配置
  quartz:
    # 使用持久化存储
    job-store-type: jdbc
    # 初始化模式,当表不存在时创建表
    jdbc:
      initialize-schema: always
    # 调度器属性
    properties:
      org:
        quartz:
          scheduler:
            instanceId: AUTO
          jobStore:
            isClustered: true

创建Job

java
@Component
public class ScheduledJob extends QuartzJobBean {
    
    @Autowired
    private UserService userService;
    
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        // 执行定时任务
        System.out.println("执行定时任务,时间:" + new Date());
        userService.updateUserStatus();
    }
}

配置JobDetail和Trigger

java
@Configuration
public class SchedulerConfig {
    
    @Bean
    public JobDetail jobDetail() {
        return JobBuilder.newJob(ScheduledJob.class)
                .withIdentity("scheduledJob", "group1")
                .storeDurably()
                .build();
    }
    
    @Bean
    public Trigger trigger() {
        return TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity("scheduledTrigger", "group1")
                .withSchedule(CronScheduleBuilder.cronSchedule("0 0 10 * * ?"))
                .build();
    }
}

7. Quartz集群配置

7.1 集群配置文件

properties
# 配置调度器实例名称
org.quartz.scheduler.instanceName = MyClusterScheduler

# 配置调度器实例ID,在集群环境中需要唯一
org.quartz.scheduler.instanceId = AUTO

# 配置线程池
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5

# 配置作业存储方式
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

# 配置驱动委托类
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

# 配置数据源
org.quartz.jobStore.dataSource = myDS

# 配置表前缀
org.quartz.jobStore.tablePrefix = QRTZ_

# 配置集群
org.quartz.jobStore.isClustered = true

# 配置检查集群状态的间隔时间(毫秒)
org.quartz.jobStore.clusterCheckinInterval = 20000

# 配置数据源
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/quartz
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = root
org.quartz.dataSource.myDS.maxConnections = 10

7.2 集群部署注意事项

  1. 共享数据库: 所有节点必须使用同一个数据库实例
  2. 表结构: 确保数据库中存在Quartz所需的表结构
  3. 唯一实例ID: 每个节点的实例ID必须唯一,可以使用AUTO自动生成
  4. 检查间隔: 合理设置集群检查间隔时间
  5. 负载均衡: Quartz集群会自动进行负载均衡,将任务分配给不同的节点执行
  6. 故障转移: 当某个节点故障时,其他节点会接管其任务

8. 错误处理与重试机制

8.1 任务重试

Quartz本身没有直接提供重试机制,但可以通过以下方式实现:

java
public class RetryJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap dataMap = context.getMergedJobDataMap();
        int retries = dataMap.getInt("retries");
        int maxRetries = dataMap.getInt("maxRetries");
        
        try {
            // 执行任务逻辑
            performTask();
        } catch (Exception e) {
            // 任务执行失败
            System.out.println("Task failed, retry count: " + retries);
            
            if (retries < maxRetries) {
                // 增加重试次数
                retries++;
                dataMap.put("retries", retries);
                
                // 创建新的JobDetail,包含更新后的重试次数
                JobDetail newJobDetail = JobBuilder.newJob(RetryJob.class)
                        .withIdentity(context.getJobDetail().getKey())
                        .usingJobData(dataMap)
                        .build();
                
                // 创建延迟执行的Trigger
                Trigger newTrigger = TriggerBuilder.newTrigger()
                        .withIdentity(context.getTrigger().getKey())
                        .startAt(new Date(System.currentTimeMillis() + 60000)) // 1分钟后重试
                        .build();
                
                try {
                    // 重新调度任务
                    Scheduler scheduler = context.getScheduler();
                    scheduler.rescheduleJob(context.getTrigger().getKey(), newTrigger);
                    scheduler.addJob(newJobDetail, true);
                } catch (SchedulerException schedulerEx) {
                    throw new JobExecutionException(schedulerEx);
                }
            } else {
                // 达到最大重试次数,放弃重试
                System.out.println("Task failed after maximum retries");
            }
        }
    }
    
    private void performTask() {
        // 任务执行逻辑
    }
}

8.2 错误处理策略

  1. 记录日志: 将错误信息记录到日志系统中
  2. 发送通知: 通过邮件、短信等方式通知管理员
  3. 自动重试: 对于临时性故障,可以实现自动重试机制
  4. 降级处理: 对于非关键任务,可以实现降级处理逻辑
  5. 熔断保护: 对于频繁失败的任务,可以实现熔断保护,避免系统资源浪费

9. Quartz的最佳实践

9.1 任务设计建议

  1. 任务粒度: 任务应该尽量小而专注,职责单一
  2. 幂等性: 任务应该设计成幂等的,避免重复执行造成问题
  3. 异常处理: 任务内部应该有完善的异常处理机制
  4. 日志记录: 任务执行过程中应该记录详细的日志,便于问题排查
  5. 超时控制: 对于可能长时间运行的任务,应该设置超时控制
  6. 资源释放: 任务执行完成后,应该及时释放占用的资源

9.2 性能优化建议

  1. 线程池配置: 根据系统负载和任务数量,合理配置线程池大小
  2. 批量处理: 对于大量小任务,可以考虑批量处理,减少调度开销
  3. 任务分组: 合理组织任务分组,便于管理和监控
  4. 避免阻塞: 任务执行过程中应避免长时间阻塞,必要时使用异步处理
  5. 定期清理: 定期清理不再使用的任务和触发器
  6. 使用持久化: 对于关键任务,建议使用持久化存储,确保可靠性

9.3 监控与维护建议

  1. 任务监控: 实现任务执行状态的监控,及时发现和处理异常任务
  2. 性能监控: 监控调度器的性能指标,如线程池使用率、任务执行时间等
  3. 日志分析: 定期分析任务执行日志,找出问题和优化点
  4. 备份配置: 定期备份任务配置信息,防止数据丢失
  5. 版本控制: 对任务代码进行版本控制,便于追踪和回滚

10. 常见问题与解决方案

10.1 任务不执行

  • 原因: 调度器未启动、触发器配置错误、任务类未找到
  • 解决方案: 检查调度器启动状态、验证触发器配置、确认任务类路径正确

10.2 任务重复执行

  • 原因: 集群配置错误、调度器实例ID冲突、数据库连接问题
  • 解决方案: 检查集群配置、确保实例ID唯一、验证数据库连接

10.3 内存占用过高

  • 原因: 任务过多、任务执行时间过长、线程池配置不合理
  • 解决方案: 合理设计任务粒度、优化任务执行逻辑、调整线程池大小

10.4 数据库锁定问题

  • 原因: 任务执行时间过长、事务未正确提交
  • 解决方案: 减少任务执行时间、确保事务正确提交、合理设置数据库锁超时时间

10.5 时区问题

  • 原因: 服务器时区配置与预期不一致
  • 解决方案: 明确指定时区配置、使用UTC时间

11. 总结

Quartz是一个功能强大、灵活可靠的任务调度框架,广泛应用于Java企业应用中。它提供了丰富的API和灵活的配置选项,可以满足各种复杂的定时任务需求。通过本文的介绍,我们了解了Quartz的核心概念、基本使用方法、高级特性以及与Spring的集成方式。在实际应用中,我们应该根据具体需求,合理配置和使用Quartz,并遵循最佳实践,确保任务调度的可靠性和性能。

基于 MIT 许可发布