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 在时间轴上的触发行为差异:

三种触发模式时间轴对比 t cron T1 T2 T3 固定日历时间 固定日历时间 fixedRate T1 T2 T3 固定间隔(从开始算) 固定间隔(从开始算) fixedDelay T1 T2 T3 固定间隔(从结束算) 固定间隔(从结束算) 说明:cron 按绝对时间触发;fixedRate 按任务开始时间间隔触发(可能重叠);fixedDelay 按任务结束时间间隔触发(永不重叠)。 实际开发中,DAG 边界任务建议优先使用 fixedDelay 或 cron,避免因重叠导致数据竞争。

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……严格串行。

2.6 任务调度执行流程架构图

下图展示从 Spring 容器启动到任务执行的完整链路:

@Scheduled 任务调度执行流程 Spring 容器启动 @EnableScheduling ScheduledAnnotation BeanPostProcessor 解析 @Scheduled 注解 注册 Runnable 任务 ThreadPoolTaskScheduler (线程池) 任务方法执行 关键点:ScheduledAnnotationBeanPostProcessor 在 postProcessAfterInitialization 阶段扫描所有 Bean,收集 @Scheduled 方法, 根据 cron/fixedRate/fixedDelay 创建周期性任务,提交给 ThreadPoolTaskScheduler 的 ScheduledThreadPoolExecutor 执行。

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 线程池隔离,防止一个任务拖垮全局调度。