SpringBoot 整合定时任务——@Scheduled 与 Quartz 实战

定时任务是后端系统里的刚需------每天凌晨备份数据库、每月初生成账单报表、定期清理过期数据。SpringBoot 内置了简单的定时任务方案,复杂场景也可以用 Quartz 来搞定。

一、@Scheduled------最简单的定时任务

SpringBoot 自带的方案,不需要任何第三方依赖

1. 开启定时任务

java 复制代码
@SpringBootApplication
@EnableScheduling  // 开启定时任务
public class SeckillApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class, args);
    }
}

2. 编写定时任务

java 复制代码
@Component
public class ScheduledTasks {

    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    /**
     * 每分钟执行一次
     */
    @Scheduled(cron = "0 * * * * ?")
    public void task1() {
        log.info("定时任务执行: {}", LocalDateTime.now());
    }

    /**
     * 每天晚上 23:30 执行(适合对账、备份)
     */
    @Scheduled(cron = "0 30 23 * * ?")
    public void dailyReport() {
        log.info("生成日报表...");
        // 生成报表逻辑
    }

    /**
     * 每月1号凌晨2点执行(适合月度账单、归档)
     */
    @Scheduled(cron = "0 0 2 1 * ?")
    public void monthlyArchive() {
        log.info("月度数据归档...");
        // 归档逻辑
    }
}

3. Cron 表达式速查

text 复制代码
格式:秒 分 时 日 月 周

0 0 12 * * ?      → 每天中午12点
0 0/5 * * * ?     → 每5分钟一次
0 0 23 * * ?      → 每天晚上11点
0 30 9 * * ?      → 每天早上9点30分
0 0 2 1 * ?       → 每月1号凌晨2点
0 15 10 ? * MON-FRI → 周一到周五上午10点15分
0 0 0/2 * * ?     → 每2小时一次

? 表示不指定(用在『周』或『日』上避免冲突)

推荐工具: 在线 cron 表达式生成器(百度搜"cron 在线"),可视化点选生成。

4. 固定间隔执行

不想写 cron,也可以用固定间隔:

java 复制代码
// 上一次执行完毕后,等3秒再执行
@Scheduled(fixedDelay = 3000)

// 上一次执行开始后,过5秒再执行(不管上一步有没有完成)
@Scheduled(fixedRate = 5000)

// 首次延迟10秒后开始执行
@Scheduled(initialDelay = 10000, fixedRate = 5000)

fixedDelay 和 fixedRate 的区别:

复制代码
fixedDelay:
  ┌────┐  ┌────┐  ┌────┐
  │执行│  │执行│  │执行│
  └────┘──└────┘──└────┘
   3秒    3秒    3秒   ← 执行完才开始计时

fixedRate:
  ┌────┐  ┌────┐  ┌────┐
  │执行│  │执行│  │执行│
  │────│──│────│──│────│
  │  5秒  │  5秒  │    ← 不管是否执行完,到点就执行

注意: 如果任务执行时间超过间隔时间,fixedRate 会并发执行,可能导致数据混乱。生产环境建议用 fixedDelay。

5. 异步执行(防止阻塞)

默认定时任务是单线程的------一个任务卡住了,其他任务也跟着卡住

java 复制代码
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("scheduled-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}
java 复制代码
@Component
public class AsyncScheduledTasks {

    @Async("taskExecutor")
    @Scheduled(cron = "0 */1 * * * ?")
    public void reportCurrentTime() {
        System.out.println("当前时间: " + LocalDateTime.now()
                + " 线程: " + Thread.currentThread().getName());
        // 即使这里 sleep 了,其他定时任务不受影响
    }

    @Async("taskExecutor")
    @Scheduled(cron = "0 */2 * * * ?")
    public void anotherTask() {
        System.out.println("另一个定时任务: " + LocalDateTime.now());
    }
}

二、Quartz------企业级定时任务

@Scheduled 适合简单场景,如果遇到这些需求就需要 Quartz:

  • 任务需要在运行时动态创建/修改/删除(比如用户自行配置推送时间)
  • 任务需要持久化到数据库(重启后不丢失)
  • 任务需要集群部署(多台机器不重复执行)

1. 集成 Quartz

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2. 编写任务

java 复制代码
public class DataBackupJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        // 获取参数
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String databaseName = dataMap.getString("databaseName");

        System.out.println("备份数据库: " + databaseName + ",时间: " + LocalDateTime.now());
        // 执行备份逻辑...
    }
}

3. 配置任务调度

java 复制代码
@Configuration
public class QuartzConfig {

    /**
     * 定义任务详情
     */
    @Bean
    public JobDetail dataBackupJobDetail() {
        JobDataMap dataMap = new JobDataMap();
        dataMap.put("databaseName", "seckill_db");

        return JobBuilder.newJob(DataBackupJob.class)
                .withIdentity("dataBackup")
                .usingJobData(dataMap)
                .storeDurably()      // 即使没有触发器也保留
                .build();
    }

    /**
     * 定义触发器(每天凌晨2点执行)
     */
    @Bean
    public Trigger dataBackupTrigger() {
        CronScheduleBuilder scheduleBuilder =
                CronScheduleBuilder.cronSchedule("0 0 2 * * ?");

        return TriggerBuilder.newTrigger()
                .forJob(dataBackupJobDetail())
                .withIdentity("dataBackupTrigger")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

4. 动态创建任务(运行时新增)

java 复制代码
@Service
public class QuartzService {

    @Autowired
    private Scheduler scheduler;

    /**
     * 动态添加定时任务
     */
    public void addJob(String jobName, String cron, JobDataMap dataMap) throws SchedulerException {
        // 创建任务
        JobDetail jobDetail = JobBuilder.newJob(DynamicJob.class)
                .withIdentity(jobName, "default")
                .usingJobData(dataMap)
                .build();

        // 创建触发器
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName + "Trigger", "default")
                .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                .build();

        // 注册到调度器
        scheduler.scheduleJob(jobDetail, trigger);
    }

    /**
     * 修改任务执行时间
     */
    public void updateCron(String jobName, String newCron) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", "default");
        CronTrigger newTrigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .withSchedule(CronScheduleBuilder.cronSchedule(newCron))
                .build();
        scheduler.rescheduleJob(triggerKey, newTrigger);
    }

    /**
     * 暂停任务
     */
    public void pauseJob(String jobName) throws SchedulerException {
        scheduler.pauseJob(JobKey.jobKey(jobName, "default"));
    }

    /**
     * 恢复任务
     */
    public void resumeJob(String jobName) throws SchedulerException {
        scheduler.resumeJob(JobKey.jobKey(jobName, "default"));
    }

    /**
     * 删除任务
     */
    public void deleteJob(String jobName) throws SchedulerException {
        scheduler.deleteJob(JobKey.jobKey(jobName, "default"));
    }
}

5. 持久化配置

Quartz 默认把任务信息存在内存里,重启后就丢了。如果要持久化,配置数据库:

yaml 复制代码
spring:
  quartz:
    job-store-type: jdbc   # 使用数据库存储
    jdbc:
      initialize-schema: always  # 自动创建 Quartz 表
    properties:
      org:
        quartz:
          scheduler:
            instanceName: DefaultQuartzScheduler
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: false

启用 JDBC 存储后,重启应用之前创建的任务自动恢复。

三、@Scheduled vs Quartz 怎么选

场景 推荐方案
固定时间执行,不需要改 ✅ @Scheduled
执行时间可能动态调整 ✅ Quartz
任务参数需要传参 ✅ @Scheduled 简单传参 / Quartz 传复杂参数
需要管理后台控制启停 ✅ Quartz
集群环境避免重复执行 ✅ Quartz + 数据库持久化
简单项目、快速开发 ✅ @Scheduled 就够了

秒杀系统场景:

  • 定时初始化库存 → @Scheduled 就够了
  • 以后要做订单超时自动取消 → 如果用户量大、需要灵活配置,用 Quartz

四、实际开发中的坑

1. 定时任务默认单线程

yaml 复制代码
# 解决方案一:配线程池
spring:
  task:
    scheduling:
      pool:
        size: 5  # 定时任务线程池大小

2. 时区问题

java 复制代码
// @Scheduled 默认使用服务器时区
// 如果服务器是 UTC,需要指定时区
@Scheduled(cron = "0 0 8 * * ?", zone = "Asia/Shanghai")

3. 任务超时

java 复制代码
// Quartz 触发器可以设置超时时间
// 如果任务执行超时,强制终止
trigger = TriggerBuilder.newTrigger()
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
        .withMisfireHandlingInstructionFireNow())
    .build();

五、完整案例:每天凌晨自动取消超时未支付订单

java 复制代码
@Component
public class OrderTimeoutJob {

    @Autowired
    private OrderService orderService;

    /**
     * 每30秒检查一次,取消超过10分钟未支付的订单
     */
    @Scheduled(fixedDelay = 30000)
    public void cancelTimeoutOrders() {
        // 找到所有超过10分钟未支付的订单
        List<SeckillOrder> timeoutOrders = orderService.lambdaQuery()
                .eq(SeckillOrder::getStatus, 0)
                .lt(SeckillOrder::getOrderTime, LocalDateTime.now().minusMinutes(10))
                .list();

        for (SeckillOrder order : timeoutOrders) {
            // 取消订单
            order.setStatus(2);  // 已取消
            orderService.updateById(order);

            // 归还库存
            productService.lambdaUpdate()
                    .setSql("stock = stock + " + order.getQuantity())
                    .eq(SeckillProduct::getId, order.getProductId())
                    .update();

            log.info("超时取消订单: {}", order.getOrderNo());
        }
    }
}

总结: 简单任务用 @Scheduled,复杂调度用 Quartz,不要两个混着用。大部分项目 @Scheduled 完全够用。


💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。