在后端开发中,定时任务属于刚需基础组件:订单超时自动关单、优惠券定时失效、日志/垃圾文件定时清理、统计报表凌晨生成、缓存定时刷新、分布式数据同步......几乎所有中后台项目都会用到。
SpringBoot 自带的定时任务方案开箱即用,无需引入中间件(如 Quartz、XXL-Job),轻量化、无依赖。
一、定时任务整体架构与适用场景
1. 核心分类
-
- 静态定时任务
-
• 基于
@Scheduled实现 -
• 规则写死在代码或配置文件,启动后不可修改
-
• 优点:简单、零依赖、开发极快
-
• 缺点:不灵活、单线程易阻塞
-
- 动态定时任务
-
-
• 基于
SchedulingConfigurer+Trigger实现 -
• 运行时可修改 cron、启停任务,无需重启服务
-
• 优点:高度灵活,支持运营后台配置
-
• 缺点:代码稍复杂,需要自己维护任务状态
-
-
- 分布式定时任务
-
-
• 如 Quartz、XXL-Job、Elastic-Job
-
• 解决集群下任务重复执行问题
-
• 本文重点讲 Spring 内置方案,分布式仅作对比
2. 典型业务场景
-
• 固定周期:每 5 分钟刷新一次统计数据
-
• 每日凌晨:2:00 清理 7 天前日志、生成日账单
-
• 超时任务:下单 30 分钟未支付自动取消
-
• 周期巡检:定时检查服务器状态、接口健康度
-
• 动态配置:运营在后台修改任务执行时间
二、@Scheduled 静态定时任务
1. 开启定时任务总开关
启动类添加
@EnableScheduling,否则定时任务不生效:goimport 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. 配置定时任务线程池
goimport 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. 完整动态定时任务实现
goimport 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. 提供接口动态控制
goimport 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 分钟的任务建议丢 MQ 或线程池异步处理。
-
- 集群部署必须防止重复执行
Spring 内置定时不支持分布式,集群下会多节点同时执行。
解决方案:
-
• 数据库乐观锁
-
• Redis 分布式锁(Redisson)
-
• 使用 XXL-Job 分布式定时任务
-
-
- 关键任务必须加监控告警
任务执行失败、超时,通过邮件/钉钉/企业微信告警。
-
- cron 尽量避开高峰整点
大量任务都在 0 点执行会导致瞬间压力过大,错开执行。
-
- 日志必须清晰
记录任务开始/结束时间、耗时、异常信息,方便排查。
九、总结
-
@EnableScheduling是定时任务开关,缺一不可;
-
@Scheduled支持fixedRate/fixedDelay/cron,生产优先用配置化 cron;
-
- 多任务必须配置线程池,避免单线程阻塞;
-
- 耗时任务用
@Async异步化,提升系统稳定性;
- 耗时任务用
-
- 动态定时基于
SchedulingConfigurer,可实现运行时修改、启停;
- 动态定时基于
-
- 生产务必做异常捕获、监控告警、分布式锁防重复。