人人都会AI编程

14.5 定时任务:@Scheduled 轻量定时、Quartz 分布式定时

更新时间:2026-07-10

定时任务是企业应用中最常见的需求之一:半夜对账、每小时同步数据、每分钟清理过期会话、每天发送报表邮件。Spring 提供了两个层级的定时方案——轻量级的 @Scheduled 适合单体或简单定时需求,集成 Quartz 则支撑分布式的、动态的、带持久化的复杂调度场景。

14.5.1 @Scheduled 轻量定时

@Scheduled 是 Spring 对 JDK ScheduledExecutorService 的声明式封装,无需额外依赖,只需两步即可启用。

1. 启用定时任务支持

在任何一个配置类上标注 @EnableScheduling,告诉 Spring 容器要扫描带有 @Scheduled 的方法。

@Configuration
@EnableScheduling
public class SchedulingConfig {
}

Spring Boot 项目会自动识别此注解,如果将它放在主启动类上也是常见做法。

2. 编写定时方法

@Scheduled 加在 Bean 的某个方法上,支持三种时间表达:

  • fixedRate:以固定频率执行,从上一次开始时间算起。
  • fixedDelay:以固定延迟执行,从上一次结束时间算起。
  • cron:使用 Cron 表达式,精确控制到秒、分、时、日、月、周。
@Component
public class SimpleTasks {

    // 每隔5秒执行一次(以开始时间算)
    @Scheduled(fixedRate = 5000)
    public void syncData() {
        System.out.println("同步数据:" + Thread.currentThread().getName());
    }

    // 上次执行结束后等3秒再执行
    @Scheduled(fixedDelay = 3000)
    public void cleanExpiredTokens() {
        // 清理逻辑
    }

    // 每天凌晨 2:30 执行
    @Scheduled(cron = "0 30 2 * * ?")
    public void dailyReport() {
        // 生成日报
    }
}

3. 配置线程池

默认情况下,@Scheduled 方法由只有一个线程的公共线程池执行。这意味着如果多个定时任务同时触发,它们会排队等待;如果一个任务长时间未完成,会阻塞其他所有任务。生产环境必须自定义线程池。

@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);                 // 核心线程数
        scheduler.setThreadNamePrefix("scheduled-");
        scheduler.setAwaitTerminationSeconds(60);
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        return scheduler;
    }
}

4. 使用 @Scheduled 的局限与注意事项

  • 单点执行@Scheduled 基于本地内存调度,多实例部署时同一任务会在每个节点各自执行一次。若要避免重复执行,需要借助分布式锁(如 Redis setnx、ShedLock 等)。
  • 动态管理困难:修改执行时间需要改代码重新发布,或借助 @ScheduledfixedDelayString 等从配置文件读取,但依然无法在运行时增删定时任务。
  • 无持久化与监控:任务执行状态、失败记录全依赖日志,没有原生的失败重试和任务追踪界面。

轻量场景下,@Scheduled 足够简单高效。但遇到动态调度、分布式任务、任务持久化和可视化监控需求时,Quartz 是更专业的选择。

14.5.2 Quartz 分布式定时

Quartz 是 Java 领域中久经考验的调度框架,支持任务持久化到数据库、集群环境下的分布式调度(多个节点争抢执行同一任务,保证不会重复触发),以及丰富的 Cron 表达式和日历排除规则。

Spring 提供了与 Quartz 的深度集成,核心是使用 SchedulerFactoryBean 作为 Quartz 调度器的生命周期管理 Bean。

1. 引入依赖

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

同时需要数据库驱动和 Quartz 专用表(官方提供了针对各数据库的建表脚本)。

2. 创建 Job

Quartz 的 Job 是实现 org.quartz.Job 接口的类。为了方便注入 Spring Bean,Spring 提供了 QuartzJobBean 基类,将 Job 也纳入容器管理。

@Component
public class OrderReconciliationJob extends QuartzJobBean {

    @Autowired
    private OrderService orderService;

    @Override
    protected void executeInternal(JobExecutionContext context) 
            throws JobExecutionException {
        // 真正的业务逻辑
        orderService.reconcileOrders();
    }
}

3. 配置 JobDetail 和 Trigger

JobDetail 描述了 Job 的身份和额外参数,Trigger 决定何时触发。Spring 提供 JobDetailFactoryBeanCronTriggerFactoryBean 来将它们注册为容器 Bean。

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail reconciliationJobDetail() {
        return JobBuilder.newJob(OrderReconciliationJob.class)
                .withIdentity("reconciliationJob")
                .storeDurably()                // 即使没有 Trigger 也持久化
                .build();
    }

    @Bean
    public Trigger reconciliationTrigger() {
        return TriggerBuilder.newTrigger()
                .forJob(reconciliationJobDetail())
                .withIdentity("reconciliationTrigger")
                .withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) // 每天2点
                .build();
    }
}

当 Spring 容器启动时,SchedulerFactoryBean 会自动创建 Quartz Scheduler,将 Scheduler 暴露为 Bean,并加载定义好的 Trigger。

4. 配置 Quartz 集群与持久化

要让 Quartz 支持分布式和任务持久化,必须配置 quartz.properties 并指向数据库。

spring:
  quartz:
    job-store-type: jdbc          # 使用数据库存储 Job 和 Trigger
    jdbc:
      initialize-schema: never    # 表结构已经手动创建
    properties:
      org:
        quartz:
          scheduler:
            instanceName: MyClusteredScheduler
            instanceId: AUTO      # 集群节点自动生成 ID
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            clusterCheckinInterval: 20000
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5

在集群模式下,多个应用节点共享同一套 Quartz 表。Quartz 通过数据库行锁保证同一个 Job 只会被一个节点成功索取并执行,若节点宕机,其他节点会发现并接管任务。

5. 动态管理任务

Quartz 的优势之一是支持运行时动态增删改查任务,通过注入 Scheduler 可以直接操作:

@Service
public class QuartzManageService {

    @Autowired
    private Scheduler scheduler;

    // 暂停一个触发器
    public void pauseJob(String triggerName, String triggerGroup) 
            throws SchedulerException {
        scheduler.pauseTrigger(TriggerKey.triggerKey(triggerName, triggerGroup));
    }

    // 动态添加一次性任务
    public void addOneTimeJob(String jobName, JobDataMap data) 
            throws SchedulerException {
        JobDetail jobDetail = JobBuilder.newJob(SomeSimpleJob.class)
                .withIdentity(jobName)
                .usingJobData(data)
                .build();
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName + "_trigger")
                .startAt(DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND))
                .build();
        scheduler.scheduleJob(jobDetail, trigger);
    }
}

结合控制台页面或 API,即可实现任务的可视化管理。

6. 监听与链式任务

Quartz 提供丰富的监听器接口,可以在任务执行前后、触发完成时植入业务逻辑,比如记录执行日志、发送报警通知。

@Component
public class JobExecutionLogListener extends JobListenerSupport {
    @Override
    public String getName() {
        return "ExecutionTimeLogListener";
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, 
            JobExecutionException jobException) {
        long duration = context.getJobRunTime();
        String jobKey = context.getJobDetail().getKey().toString();
        log.info("Job {} 执行耗时 {} ms", jobKey, duration);
        if (jobException != null) {
            log.error("Job {} 执行异常", jobKey, jobException);
        }
    }
}

然后在 SchedulerFactoryBean 中注册该监听器即可全局生效。

14.5.3 选型建议

  • @Scheduled:适合开发测试、小型项目、定时任务数量少且无需动态管理的场景。结合 @SchedulerLock(ShedLock)能简单实现多节点互斥执行。
  • Quartz:适合任务数量多、有动态调度需求、要求任务持久化、需要集群高可用的企业级场景。成熟稳定,但引入了数据库依赖和一定的复杂性。

在某些微服务架构中,也可以考虑将定时任务统一托管到专门的调度中心(如 XXL-JOB、Elastic-Job),进一步降低与业务代码的耦合。但就 Spring 生态内部而言,@Scheduled + Quartz 已经覆盖了绝大多数定时调度需求。