SpringBoot 定时任务:@Scheduled 基础与动态定时

在后端开发中,定时任务属于刚需基础组件:订单超时自动关单、优惠券定时失效、日志/垃圾文件定时清理、统计报表凌晨生成、缓存定时刷新、分布式数据同步......几乎所有中后台项目都会用到。

SpringBoot 自带的定时任务方案开箱即用,无需引入中间件(如 Quartz、XXL-Job),轻量化、无依赖。


一、定时任务整体架构与适用场景

1. 核心分类

    1. 静态定时任务
  • • 基于 @Scheduled 实现

  • • 规则写死在代码或配置文件,启动后不可修改

  • • 优点:简单、零依赖、开发极快

  • • 缺点:不灵活、单线程易阻塞

    1. 动态定时任务
    • • 基于 SchedulingConfigurer + Trigger 实现

    • • 运行时可修改 cron、启停任务,无需重启服务

    • • 优点:高度灵活,支持运营后台配置

    • • 缺点:代码稍复杂,需要自己维护任务状态

    1. 分布式定时任务
    • • 如 Quartz、XXL-Job、Elastic-Job

    • • 解决集群下任务重复执行问题

    • • 本文重点讲 Spring 内置方案,分布式仅作对比

    2. 典型业务场景

    • • 固定周期:每 5 分钟刷新一次统计数据

    • • 每日凌晨:2:00 清理 7 天前日志、生成日账单

    • • 超时任务:下单 30 分钟未支付自动取消

    • • 周期巡检:定时检查服务器状态、接口健康度

    • • 动态配置:运营在后台修改任务执行时间


    二、@Scheduled 静态定时任务

    1. 开启定时任务总开关

    启动类添加 @EnableScheduling,否则定时任务不生效:

    go 复制代码
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication
    @EnableScheduling   // 开启 Spring 定时任务
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }

    2. @Scheduled 三大核心参数

    @Scheduled 共支持 3 种触发规则,不可同时混用,只能选其一。

    (1)fixedRate:固定频率执行
    • • 含义:从上一次任务开始时间计算,每隔指定毫秒执行一次

    • • 任务执行时间 > 间隔时间 → 任务会排队,不会并行

    go 复制代码
    // 每 5 秒执行一次(从上一次启动时算)
    @Scheduled(fixedRate = 5000)
    public void taskFixedRate() {
        System.out.println("fixedRate 每5秒执行:" + LocalDateTime.now());
    }
    (2)fixedDelay:固定延迟执行
    • • 含义:从上一次任务执行结束时间计算,等待指定毫秒再执行

    • • 保证任务串行,不会重叠

    go 复制代码
    // 上一次执行完,等 5 秒再执行
    @Scheduled(fixedDelay = 5000)
    public void taskFixedDelay() {
        System.out.println("fixedDelay 执行完延迟5秒:" + LocalDateTime.now());
    }
    (3)cron:Cron 表达式
    • • 支持秒级复杂规则:每天、每周、每月、指定时分秒

    • • 格式:秒 分 时 日 月 周

    go 复制代码
    // 每 10 秒执行一次
    @Scheduled(cron = "0/10 * * * * ?")
    public void taskCron() {
        System.out.println("cron 每10秒执行:" + LocalDateTime.now());
    }

    3. 初始延迟:initialDelay

    项目启动后,不立即执行,等待一段时间再开始:

    go 复制代码
    // 项目启动延迟 3 秒后,每 5 秒执行一次
    @Scheduled(initialDelay = 3000, fixedRate = 5000)
    public void taskInitialDelay() {
        System.out.println("启动延迟3秒后执行");
    }

    4. Cron 表达式完整详解

    语法结构
    go 复制代码
    秒(0-59) 分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(1-7,1=周日,7=周六)
    常用符号
    • *:每一秒/每一分/每一时刻

    • ?:不指定(日和周互斥,必须有一个为 ?)

    • /:步长,如 0/5 表示从 0 开始,每 5 单位

    • -:范围,如 10-20 表示 10 到 20

    • ,:枚举,如 1,3,5 表示 1、3、5

    示例
    go 复制代码
    "0 0 2 * * ?"        = 每天凌晨 2 点
    "0 0 0 * * ?"        = 每天午夜 0 点
    "0 0/5 * * * ?"      = 每 5 分钟
    "0 0 12 * * ?"       = 每天中午 12 点
    "0 0 8 ? * MON-FRI"  = 工作日早上 8 点
    "0 0 0 1 * ?"        = 每月 1 号零点
    "0/1 * * * * ?"      = 每秒执行

    5. 从配置文件读取

    硬编码 cron 不利于维护,推荐放到 application.yml

    go 复制代码
    # 定时任务配置
    scheduled:
      task1:
        cron: "0/5 * * * * ?"
      task2:
        fixed-rate: 10000
        initial-delay: 5000

    使用:

    go 复制代码
    @Scheduled(cron = "${scheduled.task1.cron}")
    public void configTask() {
        System.out.println("从yml读取cron执行");
    }

    三、解决单线程阻塞问题

    Spring 定时任务默认是单线程,多个任务会排队,一个卡死全部卡住。

    1. 配置定时任务线程池

    go 复制代码
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
    
    @Configuration
    @EnableScheduling
    public class ScheduledThreadPoolConfig {
    
        @Bean
        public ThreadPoolTaskScheduler taskScheduler() {
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
            // 核心线程数,根据任务数量设置
            scheduler.setPoolSize(10);
            // 线程名称前缀,方便日志排查
            scheduler.setThreadNamePrefix("scheduled-task-");
            // 等待任务完成再关闭
            scheduler.setWaitForTasksToCompleteOnShutdown(true);
            // 关闭最大等待时间
            scheduler.setAwaitTerminationSeconds(60);
            scheduler.initialize();
            return scheduler;
        }
    }

    配置后,多个任务可并行执行,互不阻塞。


    四、异步 + 定时(@Async + @Scheduled)

    让定时任务异步执行,不占用调度线程,进一步提升稳定性。

    1. 开启异步

    启动类加 @EnableAsync

    go 复制代码
    @SpringBootApplication
    @EnableScheduling
    @EnableAsync   // 开启异步
    public class Application {}

    2. 异步定时任务

    go 复制代码
    @Component
    public class AsyncScheduleTask {
    
        @Async
        @Scheduled(cron = "0/5 * * * * ?")
        public void asyncTask() {
            System.out.println("异步定时任务执行:" + Thread.currentThread().getName());
        }
    }

    优点:

    • • 任务执行耗时不影响调度

    • • 异常不会导致调度线程崩溃


    五、动态定时任务

    @Scheduled 启动后不可改 cron,运营后台配置、动态调整必须用动态定时。

    1. 核心原理

    • • 实现 SchedulingConfigurer

    • • 重写 configureTasks 注册任务

    • • 使用 CronTrigger 动态读取最新 cron

    • • 把 cron 存在 DB/Redis/Nacos 中,实现真正可配置

    2. 完整动态定时任务实现

    go 复制代码
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.SchedulingConfigurer;
    import org.springframework.scheduling.config.ScheduledTaskRegistrar;
    import org.springframework.scheduling.support.CronTrigger;
    import java.time.LocalDateTime;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    @Slf4j
    @Configuration
    public class DynamicScheduleConfig implements SchedulingConfigurer {
    
        // 动态 cron(真实项目从数据库/Redis读取)
        private String cron = "0/5 * * * * ?";
    
        // 任务启停开关
        private final AtomicBoolean taskEnabled = new AtomicBoolean(true);
    
        // 外部修改 cron
        public void setCron(String cron) {
            this.cron = cron;
            log.info("动态修改cron成功:{}", cron);
        }
    
        // 启停任务
        public void setTaskEnabled(boolean enabled) {
            this.taskEnabled.set(enabled);
            log.info("任务状态已修改:{}", enabled ? "开启" : "关闭");
        }
    
        @Override
        public void configureTasks(ScheduledTaskRegistrar registrar) {
            // 注册动态任务
            registrar.addTriggerTask(
                // 任务业务逻辑
                () -> {
                    if (!taskEnabled.get()) {
                        log.info("任务已关闭,跳过执行");
                        return;
                    }
                    log.info("动态定时任务执行:{}", LocalDateTime.now());
                    // 执行业务逻辑...
                },
                // 动态触发器:每次执行前重新获取cron
                triggerContext -> {
                    CronTrigger cronTrigger = new CronTrigger(cron);
                    return cronTrigger.nextExecutionTime(triggerContext);
                }
            );
        }
    }

    3. 提供接口动态控制

    go 复制代码
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/schedule")
    public class ScheduleController {
    
        @Autowired
        private DynamicScheduleConfig dynamicScheduleConfig;
    
        // 动态修改 cron
        @GetMapping("/updateCron")
        public String updateCron(@RequestParam String cron) {
            try {
                dynamicScheduleConfig.setCron(cron);
                return "修改成功,新cron:" + cron;
            } catch (Exception e) {
                return "cron 格式非法:" + e.getMessage();
            }
        }
    
        // 开启/关闭任务
        @GetMapping("/enable")
        public String enableTask(@RequestParam boolean enabled) {
            dynamicScheduleConfig.setTaskEnabled(enabled);
            return "任务已" + (enabled ? "开启" : "关闭");
        }
    }

    测试接口:

    go 复制代码
    # 修改为每10秒执行一次
    http://localhost:8080/schedule/updateCron?cron=0/10 * * * * ?
    
    # 关闭任务
    http://localhost:8080/schedule/enable?enabled=false

    无需重启,立即生效!


    六、定时任务异常处理

    定时任务一旦抛异常且未捕获,会导致后续调度失效,必须做异常防护。

    1. 统一 try-catch 防护

    go 复制代码
    @Scheduled(cron = "${scheduled.task1.cron}")
    public void safeTask() {
        try {
            // 业务逻辑
            System.out.println("任务执行");
        } catch (Exception e) {
            // 记录日志 + 告警
            log.error("定时任务执行异常", e);
        }
    }

    2. 全局异常捕获

    结合 AOP 或全局异常切面统一捕获,避免每个任务写 try-catch。


    七、静态 vs 动态 方案对比

    方案 优点 缺点 适用场景
    @Scheduled 静态 极简、零依赖、上手快 不可修改、单线程阻塞 固定周期、不常变更的任务
    动态定时(Trigger) 运行可改、可启停、灵活配置 代码稍复杂 运营后台配置、动态调整、多环境适配
    异步定时 不阻塞调度、高可用 需注意线程安全、事务 耗时任务、批量处理任务

    八、注意事项

      1. 禁止在定时任务中写超大耗时操作

      超过 1 分钟的任务建议丢 MQ 或线程池异步处理。

      1. 集群部署必须防止重复执行

      Spring 内置定时不支持分布式,集群下会多节点同时执行。

      解决方案:

    • • 数据库乐观锁

    • • Redis 分布式锁(Redisson)

    • • 使用 XXL-Job 分布式定时任务

    1. 关键任务必须加监控告警

    任务执行失败、超时,通过邮件/钉钉/企业微信告警。

    1. cron 尽量避开高峰整点

    大量任务都在 0 点执行会导致瞬间压力过大,错开执行。

    1. 日志必须清晰

    记录任务开始/结束时间、耗时、异常信息,方便排查。


九、总结

    1. @EnableScheduling 是定时任务开关,缺一不可;
    1. @Scheduled 支持 fixedRate/fixedDelay/cron,生产优先用配置化 cron;
    1. 多任务必须配置线程池,避免单线程阻塞;
    1. 耗时任务用 @Async 异步化,提升系统稳定性;
    1. 动态定时基于 SchedulingConfigurer,可实现运行时修改、启停;
    1. 生产务必做异常捕获、监控告警、分布式锁防重复
相关推荐
派大星酷2 小时前
跨域是什么 有什么影响 怎么解决
java·网络
CV艺术家2 小时前
mysql数据迁移到达梦数据库
java·数据库
我叫黑大帅2 小时前
如何设计应用层 ACK 来补充 TCP 的不足?
后端·面试·go
wuqingshun3141592 小时前
说一下mybatis里面#{}和${}的区别
java·spring·mybatis
AIUCE2 小时前
我给 AI 装了个"秦始皇":11 层架构解决 AI 黑箱问题
后端
SimonKing2 小时前
每天白送4000万Token!这款“龙虾”AI神器,微信就能操控电脑
java·后端·程序员
ACGkaka_2 小时前
SpringBoot 实战(四十一):集成 Elasticsearch
spring boot·elasticsearch·jenkins
橘子编程2 小时前
编程语言全指南:从C到Rust
java·c语言·开发语言·c++·python·rust·c#
ego.iblacat2 小时前
Flask 框架
后端·python·flask