Skip to content

Spring Boot Quartz 任务调度器:企业级定时任务的终极解决方案 🚀

引言:为什么我们需要 Quartz?

想象一下这些场景:

  • 每天凌晨 2 点自动备份数据库
  • 每小时清理临时文件
  • 每月生成财务报表
  • 定期发送营销邮件

如果没有专业的任务调度器,我们可能会写出这样的代码:

kotlin
// 糟糕的做法 - 不要这样做!
@Component
class BadScheduler {
    
    @Scheduled(fixedRate = 3600000) // 每小时执行一次
    fun cleanupTask() {
        // 如果服务重启,任务状态丢失
        // 无法动态修改执行时间
        // 无法查看任务执行历史
        // 集群环境下会重复执行
    }
}

WARNING

简单的 @Scheduled 注解虽然方便,但在企业级应用中存在诸多限制:任务状态不可持久化、无法动态管理、集群支持不完善等问题。

Quartz 的设计哲学:提供一个功能强大、可扩展、企业级的任务调度解决方案,让复杂的定时任务管理变得简单而可靠。

Quartz 核心概念解析 ⚙️

三大核心组件

NOTE

JobDetail 定义"做什么",Trigger 定义"什么时候做",Scheduler 负责协调整个执行过程。

Spring Boot 集成 Quartz

1. 依赖配置

首先添加 Quartz 启动器依赖:

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-quartz")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa") // 如需持久化
}

2. 基础配置

yaml
spring:
  quartz:
    job-store-type: memory # 默认配置,重启后任务丢失
    properties:
      org:
        quartz:
          threadPool:
            threadCount: 10 # 线程池大小
yaml
spring:
  quartz:
    job-store-type: jdbc # 持久化存储
    jdbc:
      initialize-schema: always # 自动初始化数据库表
    properties:
      org:
        quartz:
          threadPool:
            threadCount: 20
          jobStore:
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            useProperties: false
            tablePrefix: QRTZ_
            isClustered: true # 集群支持
            clusterCheckinInterval: 10000

IMPORTANT

JDBC 存储模式下,Quartz 会自动创建相关数据表。在生产环境中,建议手动管理数据库 schema 而不是使用 initialize-schema: always

实战案例:构建企业级任务调度系统

场景 1:数据清理任务

kotlin
import org.quartz.JobExecutionContext
import org.springframework.scheduling.quartz.QuartzJobBean
import org.springframework.stereotype.Component

/**
 * 数据清理任务 - 清理过期的临时数据
 */
@Component
class DataCleanupJob : QuartzJobBean() {
    
    private lateinit var dataService: DataService
    private var retentionDays: Int = 30
    
    // Spring 会自动注入 Bean
    fun setDataService(dataService: DataService) { 
        this.dataService = dataService
    }
    
    // 从 JobDataMap 注入参数
    fun setRetentionDays(retentionDays: Int) { 
        this.retentionDays = retentionDays
    }
    
    override fun executeInternal(context: JobExecutionContext) {
        val startTime = System.currentTimeMillis()
        
        try {
            // 执行清理逻辑
            val deletedCount = dataService.cleanupExpiredData(retentionDays)
            
            val duration = System.currentTimeMillis() - startTime
            println("数据清理完成: 删除 $deletedCount 条记录,耗时 ${duration}ms") 
            
        } catch (e: Exception) {
            println("数据清理失败: ${e.message}") 
            throw e // 让 Quartz 知道任务执行失败
        }
    }
}

场景 2:任务配置类

kotlin
import org.quartz.*
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class QuartzConfig {
    
    /**
     * 数据清理任务配置
     */
    @Bean
    fun dataCleanupJobDetail(): JobDetail {
        return JobBuilder.newJob(DataCleanupJob::class.java)
            .withIdentity("dataCleanupJob", "maintenance") 
            .withDescription("清理过期数据任务")
            .usingJobData("retentionDays", 7) // 保留7天的数据
            .storeDurably() // 即使没有触发器也保持任务
            .build()
    }
    
    /**
     * 每天凌晨2点执行
     */
    @Bean
    fun dataCleanupTrigger(): Trigger {
        return TriggerBuilder.newTrigger()
            .forJob(dataCleanupJobDetail())
            .withIdentity("dataCleanupTrigger", "maintenance")
            .withDescription("每天凌晨2点执行数据清理")
            .withSchedule(
                CronScheduleBuilder.cronSchedule("0 0 2 * * ?") 
                    .withMisfireHandlingInstructionDoNothing() // 错过执行时间则跳过
            )
            .build()
    }
    
    /**
     * 报表生成任务 - 每月1号执行
     */
    @Bean
    fun monthlyReportJobDetail(): JobDetail {
        return JobBuilder.newJob(MonthlyReportJob::class.java)
            .withIdentity("monthlyReportJob", "reports")
            .withDescription("月度报表生成任务")
            .storeDurably()
            .build()
    }
    
    @Bean
    fun monthlyReportTrigger(): Trigger {
        return TriggerBuilder.newTrigger()
            .forJob(monthlyReportJobDetail())
            .withIdentity("monthlyReportTrigger", "reports")
            .withSchedule(
                CronScheduleBuilder.cronSchedule("0 0 1 1 * ?") // 每月1号凌晨执行
            )
            .build()
    }
}

场景 3:动态任务管理服务

kotlin
import org.quartz.*
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.ZoneId

@Service
class TaskManagementService(
    private val scheduler: Scheduler // Spring Boot 自动配置的调度器
) {
    
    /**
     * 动态创建一次性任务
     */
    fun scheduleOneTimeTask(
        jobClass: Class<out Job>,
        executeTime: LocalDateTime,
        jobData: Map<String, Any> = emptyMap()
    ): String {
        val jobKey = JobKey.jobKey("oneTime_${System.currentTimeMillis()}", "dynamic")
        
        // 创建任务详情
        val jobDetail = JobBuilder.newJob(jobClass)
            .withIdentity(jobKey)
            .withDescription("动态创建的一次性任务")
            .apply {
                jobData.forEach { (key, value) ->
                    usingJobData(key, value.toString()) 
                }
            }
            .build()
        
        // 创建触发器
        val trigger = TriggerBuilder.newTrigger()
            .withIdentity("trigger_${jobKey.name}", "dynamic")
            .startAt(java.util.Date.from(executeTime.atZone(ZoneId.systemDefault()).toInstant())) 
            .build()
        
        // 调度任务
        scheduler.scheduleJob(jobDetail, trigger)
        
        return jobKey.name
    }
    
    /**
     * 暂停任务
     */
    fun pauseJob(jobName: String, groupName: String = "DEFAULT"): Boolean {
        return try {
            scheduler.pauseJob(JobKey.jobKey(jobName, groupName)) 
            true
        } catch (e: SchedulerException) {
            println("暂停任务失败: ${e.message}") 
            false
        }
    }
    
    /**
     * 恢复任务
     */
    fun resumeJob(jobName: String, groupName: String = "DEFAULT"): Boolean {
        return try {
            scheduler.resumeJob(JobKey.jobKey(jobName, groupName)) 
            true
        } catch (e: SchedulerException) {
            println("恢复任务失败: ${e.message}") 
            false
        }
    }
    
    /**
     * 获取所有任务状态
     */
    fun getAllJobStatus(): List<JobStatus> {
        val jobStatuses = mutableListOf<JobStatus>()
        
        scheduler.jobGroupNames.forEach { groupName ->
            scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)).forEach { jobKey ->
                val triggers = scheduler.getTriggersOfJob(jobKey)
                val triggerState = if (triggers.isNotEmpty()) {
                    scheduler.getTriggerState(triggers[0].key) 
                } else {
                    Trigger.TriggerState.NONE
                }
                
                jobStatuses.add(
                    JobStatus(
                        jobName = jobKey.name,
                        groupName = jobKey.group,
                        state = triggerState.name,
                        nextFireTime = triggers.firstOrNull()?.nextFireTime
                    )
                )
            }
        }
        
        return jobStatuses
    }
}

data class JobStatus(
    val jobName: String,
    val groupName: String,
    val state: String,
    val nextFireTime: java.util.Date?
)

高级特性与最佳实践

1. 集群配置

集群部署的优势

  • 高可用性:单个节点故障不影响任务执行
  • 负载均衡:任务在多个节点间分布执行
  • 故障转移:自动检测节点状态并重新分配任务
yaml
spring:
  quartz:
    properties:
      org:
        quartz:
          jobStore:
            isClustered: true
            clusterCheckinInterval: 10000 # 集群检查间隔(毫秒)
          scheduler:
            instanceId: AUTO # 自动生成实例ID
            instanceName: MyClusterScheduler

2. 自定义数据源配置

kotlin
@Configuration
class QuartzDataSourceConfig {
    
    /**
     * Quartz 专用数据源
     */
    @Bean
    @QuartzDataSource
    @ConfigurationProperties("spring.datasource.quartz")
    fun quartzDataSource(): DataSource {
        return DataSourceBuilder.create().build()
    }
    
    /**
     * Quartz 专用事务管理器
     */
    @Bean
    @QuartzTransactionManager
    fun quartzTransactionManager(@QuartzDataSource dataSource: DataSource): PlatformTransactionManager {
        return DataSourceTransactionManager(dataSource)
    }
}

3. 任务监听器

kotlin
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import org.quartz.JobListener
import org.springframework.stereotype.Component

@Component
class GlobalJobListener : JobListener {
    
    override fun getName(): String = "GlobalJobListener"
    
    override fun jobToBeExecuted(context: JobExecutionContext) {
        val jobKey = context.jobDetail.key
        println("任务即将执行: ${jobKey.group}.${jobKey.name}") 
    }
    
    override fun jobExecutionVetoed(context: JobExecutionContext) {
        val jobKey = context.jobDetail.key
        println("任务执行被否决: ${jobKey.group}.${jobKey.name}") 
    }
    
    override fun jobWasExecuted(
        context: JobExecutionContext,
        jobException: JobExecutionException?
    ) {
        val jobKey = context.jobDetail.key
        val duration = context.jobRunTime
        
        if (jobException != null) {
            println("任务执行失败: ${jobKey.group}.${jobKey.name}, 异常: ${jobException.message}") 
        } else {
            println("任务执行成功: ${jobKey.group}.${jobKey.name}, 耗时: ${duration}ms") 
        }
    }
}

// 注册监听器
@Configuration
class QuartzListenerConfig {
    
    @Bean
    fun schedulerFactoryBeanCustomizer(jobListener: GlobalJobListener): SchedulerFactoryBeanCustomizer {
        return SchedulerFactoryBeanCustomizer { schedulerFactoryBean ->
            schedulerFactoryBean.setGlobalJobListeners(jobListener) 
        }
    }
}

常见问题与解决方案

1. 任务重复执行问题

WARNING

在集群环境中,如果时钟不同步或网络延迟,可能导致任务重复执行。

解决方案

kotlin
@Component
class SafeExecutionJob : QuartzJobBean() {
    
    override fun executeInternal(context: JobExecutionContext) {
        val jobKey = context.jobDetail.key
        val lockKey = "${jobKey.group}_${jobKey.name}_${context.fireTime.time}"
        
        // 使用分布式锁确保任务只执行一次
        if (distributedLockService.tryLock(lockKey, Duration.ofMinutes(30))) { 
            try {
                // 执行实际业务逻辑
                performBusinessLogic()
            } finally {
                distributedLockService.unlock(lockKey) 
            }
        } else {
            println("任务已在其他节点执行,跳过: $lockKey") 
        }
    }
}

2. 任务执行时间过长

kotlin
@Component
class LongRunningJob : QuartzJobBean() {
    
    override fun executeInternal(context: JobExecutionContext) {
        val maxExecutionTime = Duration.ofHours(2)
        val startTime = System.currentTimeMillis()
        
        try {
            // 分批处理大量数据
            val batchSize = 1000
            var processedCount = 0
            
            while (hasMoreData() && !isTimeExceeded(startTime, maxExecutionTime)) {
                val batch = getNextBatch(batchSize)
                processBatch(batch) 
                processedCount += batch.size
                
                // 定期检查是否需要中断
                if (Thread.currentThread().isInterrupted) { 
                    println("任务被中断,已处理: $processedCount 条记录")
                    break
                }
            }
            
        } catch (e: Exception) {
            println("长时间运行任务异常: ${e.message}") 
            throw e
        }
    }
    
    private fun isTimeExceeded(startTime: Long, maxDuration: Duration): Boolean {
        return System.currentTimeMillis() - startTime > maxDuration.toMillis()
    }
}

性能优化建议

1. 线程池调优

yaml
spring:
  quartz:
    properties:
      org:
        quartz:
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 25 # 根据任务并发需求调整
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true

2. 数据库连接池优化

yaml
spring:
  datasource:
    quartz:
      hikari:
        maximum-pool-size: 10 # Quartz 专用连接池
        minimum-idle: 5
        connection-timeout: 30000
        idle-timeout: 600000
        max-lifetime: 1800000

总结

Quartz 作为企业级任务调度解决方案,为我们提供了:

可靠性:支持持久化存储,服务重启任务不丢失
可扩展性:集群支持,轻松应对高并发场景
灵活性:丰富的触发器类型,满足各种调度需求
可管理性:完善的任务监控和动态管理能力

TIP

在选择任务调度方案时,简单场景可以使用 @Scheduled,但对于企业级应用,Quartz 是更好的选择。它不仅解决了定时执行的问题,更提供了完整的任务生命周期管理能力。

通过 Spring Boot 的自动配置,我们可以快速集成 Quartz,构建出稳定可靠的任务调度系统,为业务发展提供强有力的技术支撑! 🎉