2. Spring Boot 任务调度基础:@Scheduled 注解详解、cron 表达式、fixedRate 与 fixedDelay 区别
Spring Boot 通过 @EnableScheduling 激活调度能力,再配合 @Scheduled 注解即可声明式地定义定时任务。本章深入拆解注解的三种触发模式、cron 表达式的构成规则,以及 fixedRate 与 fixedDelay 的核心差异——这些是构建可靠 DAG 调度系统的基石。
2.1 @Scheduled 注解核心参数
@Scheduled 注解定义在无参、返回 void 的 Bean 方法上。Spring 容器启动后,由 ScheduledAnnotationBeanPostProcessor 解析并注册到 TaskScheduler 线程池中。
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
| cron | String | 基于 cron 表达式的日历级触发 | "0 0/5 * * * ?" |
| fixedRate | long | 两次任务开始之间的固定间隔(毫秒) | 5000 |
| fixedDelay | long | 上次任务结束到下次任务开始的固定间隔(毫秒) | 5000 |
| initialDelay | long | 首次执行前的延迟时间(毫秒),常与 fixedRate/fixedDelay 配合 | 10000 |
| zone | String | cron 表达式使用的时区,默认服务器本地时区 | "Asia/Shanghai" |
关键原则: 所有定时任务默认运行在同一个
ThreadPoolTaskScheduler 线程池中(默认单线程)。若任务阻塞,后续任务将被排队延迟。生产环境务必自定义线程池大小。
2.2 cron 表达式详解
cron 表达式由 6 或 7 个空格分隔的字段组成,按秒、分、时、日、月、周、年(可选)顺序排列。
| 位置 | 字段 | 允许值 | 通配符 |
|---|---|---|---|
| 1 | 秒 | 0–59 | , - * / |
| 2 | 分 | 0–59 | , - * / |
| 3 | 时 | 0–23 | , - * / |
| 4 | 日 | 1–31 | , - * ? / L W |
| 5 | 月 | 1–12 或 JAN–DEC | , - * / |
| 6 | 周 | 0–7 或 SUN–SAT(0 和 7 都表示周日) | , - * ? / L # |
| 7 | 年(可选) | 1970–2099 | , - * / |
常用通配符含义:
*:匹配所有值?:不指定值(仅用于日/周字段,避免冲突)-:范围,如10-15/:步长,如0/5表示从 0 开始每 5 单位触发L:最后一天(日字段为当月最后一天,周字段为周六)W:最近的工作日#:第几个星期几,如2#1表示第一个星期一
实战口诀: “日和周互斥用 ?,步长范围用 / -,L 表最后 W 最近,# 定位第几周几。”
常见 cron 示例:
| 表达式 | 含义 |
|---|---|
"0 0 2 * * ?" |
每天凌晨 2:00 执行 |
"0 0/15 * * * ?" |
每 15 分钟执行一次 |
"0 0 9-18 * * MON-FRI" |
工作日 9:00–18:00 每小时执行 |
"0 0 0 L * ?" |
每月最后一天午夜执行 |
"0 0 0 ? * 2#1" |
每月第一个星期一午夜执行 |
2.3 fixedRate 与 fixedDelay 核心区别
这是任务调度中最容易混淆的概念,也是 DAG 边界设计中决定任务是否允许重叠的关键参数。
| 对比维度 | fixedRate | fixedDelay |
|---|---|---|
| 计时起点 | 任务开始时刻 | 任务结束时刻 |
| 间隔含义 | 两次开始之间的固定时间 | 上次结束到下次开始之间的固定时间 |
| 任务重叠 | 若任务执行时间 > 间隔,允许重叠(默认单线程下排队) | 天然禁止重叠,必须等上次完成 |
| 适用场景 | 心跳、状态上报、固定频率轮询 | 资源清理、数据同步、需要严格串行的批处理 |
| 线程池影响 | 多线程下可能并发执行同一任务 | 始终串行执行 |
避坑提示: 默认单线程池下,fixedRate 若任务执行超时,后续任务不会并发执行,而是等待当前任务完成后再立即触发——此时实际间隔可能远小于配置值,造成“追赶效应”。务必根据业务容忍度选择模式。
2.4 三种触发模式对比图
下图直观展示 cron、fixedRate、fixedDelay 在时间轴上的触发行为差异:
2.5 代码实战:三种模式完整示例
@Component
@EnableScheduling
public class ScheduledTaskDemo {
// 1. cron 表达式:每天 12:00 执行
@Scheduled(cron = "0 0 12 * * ?")
public void reportDailySummary() {
System.out.println("[cron] 生成日报,时间:" + LocalDateTime.now());
}
// 2. fixedRate:每 5 秒执行一次,不考虑任务耗时
@Scheduled(fixedRate = 5000)
public void heartbeat() {
System.out.println("[fixedRate] 心跳上报,时间:" + LocalDateTime.now());
// 模拟耗时 2 秒
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
// 3. fixedDelay:上次执行完后等待 3 秒再执行下一次
@Scheduled(fixedDelay = 3000, initialDelay = 5000)
public void cleanTempFiles() {
System.out.println("[fixedDelay] 清理临时文件,时间:" + LocalDateTime.now());
// 模拟耗时 4 秒
try { Thread.sleep(4000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
执行结果分析:
- fixedRate(5000):任务执行 2 秒,但间隔从开始算,所以实际 T1 开始 0s,T2 开始 5s,T3 开始 10s……任务不会重叠。
- fixedDelay(3000):任务执行 4 秒,结束后等待 3 秒再开始下一次。T1 开始 5s(initialDelay),结束 9s,T2 开始 12s,结束 16s,T3 开始 19s……严格串行。
- fixedRate(5000):任务执行 2 秒,但间隔从开始算,所以实际 T1 开始 0s,T2 开始 5s,T3 开始 10s……任务不会重叠。
- fixedDelay(3000):任务执行 4 秒,结束后等待 3 秒再开始下一次。T1 开始 5s(initialDelay),结束 9s,T2 开始 12s,结束 16s,T3 开始 19s……严格串行。
2.6 任务调度执行流程架构图
下图展示从 Spring 容器启动到任务执行的完整链路:
2.7 常见故障与排查要点
| 故障现象 | 可能原因 | 排查命令/手段 |
|---|---|---|
| 任务未执行 | 未加 @EnableScheduling;Bean 未被扫描;cron 表达式错误 |
检查启动类注解;查看日志 Initializing ExecutorService |
| 任务执行延迟 | 线程池默认单线程,前序任务阻塞 | 自定义 TaskScheduler 线程池大小;检查任务耗时 |
| fixedRate 任务重叠 | 任务耗时 > 间隔,且线程池 > 1 | 改用 fixedDelay;或加 @Lock 分布式锁 |
| cron 触发时间不对 | 时区未指定;服务器时区与预期不符 | 设置 zone = "Asia/Shanghai";检查 TimeZone.getDefault() |
| 任务抛出异常后不再执行 | 默认 ScheduledThreadPoolExecutor 会取消后续执行 |
在任务内 try-catch 所有异常;使用 ErrorHandler |
生产建议: 所有定时任务方法体最外层必须包裹 try-catch,避免未捕获异常导致任务永久停止。同时建议为每个任务配置独立的
@Scheduled 线程池隔离,防止一个任务拖垮全局调度。