最后更新:2026-10-28
适用场景:Spring Boot 3.x、@Scheduled、Jar / 容器部署

定时清理在本地每天早上九点正常跑,部署后却像消失了一样。另一个环境恰好相反:每次都跑两遍。先别急着换 Quartz。这两种现象通常可以从任务有没有被注册、触发时间怎么算、实例到底有几个三件事里找到答案。

先把“等到九点”改成五秒一次

在测试环境用一个只记日志、不改业务数据的任务排除时间因素:

@SpringBootApplication
@EnableScheduling
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@Component
public class ScheduleProbe {
    private static final Logger log =
            LoggerFactory.getLogger(ScheduleProbe.class);

    @Scheduled(fixedDelay = 5000)
    public void tick() {
        log.info("schedule probe, thread={}", Thread.currentThread().getName());
    }
}

上面省略了常规 import,两个类分别放在自己的 Java 文件中。启动后等十几秒,应该看到多次 schedule probe。确认后删除探针,别让它在生产环境长期刷日志。Spring Framework 的调度文档明确要求启用调度,并由容器管理带 @Scheduled 的 Bean。

如果探针也没动,先查 @EnableScheduling 是否在实际启动的配置中;ScheduleProbe 是否在启动类的包或子包里;类是否有 @Component 或经 @Bean 注册。把对象自己 new 出来,注解不会因此被 Spring 扫到。包结构的基本规则可参考 Spring Boot 官方说明。

探针能跑,原任务不跑:看触发条件

Spring 的 cron 是 六段,第一段是秒,不是 Linux crontab 的五段。例如工作日九点:

@Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Shanghai")
public void sendReport() {
    // 业务逻辑
}

在服务器上先打印当前时间和时区,再核对任务配置。上面的 zone 让 cron 按上海时间解释,不会依赖容器默认时区。fixedDelay 是上一次执行结束后再等指定时长;fixedRate 则按开始时间间隔安排。任务本身跑了三分钟,却用一分钟 fixedDelay,并不会每分钟触发一次。

还要检查配置占位符是否读到了预期值、线上激活了哪个 profile。比如 cron 写在 application-prod.yml,线上没有启用 prod,你改这份文件不会改变运行中的任务。类似环境问题可对照Spring Boot 配置文件怎么写。

任务注册了,却一直轮不到它

Spring Boot 3.5 在普通平台线程配置下,自动配置的 ThreadPoolTaskScheduler 默认只有 1 个线程。一个长时间阻塞的定时任务,可能把别的任务排在后面。先在每个任务开始和结束时记录时间、线程名与耗时,看看是不是某个任务一直没结束。

确认确实需要并行后,再评估线程池大小:

spring:
  task:
    scheduling:
      pool:
        size: 2

别把线程数直接调到几十。并行后可能出现共享数据竞争,原任务若没有幂等保护,跑得更快不一定更对。使用 Java 21 虚拟线程模式时,调度器实现与这些池配置也不同,应以实际配置为准。Spring Boot 调度说明列出了这些差异。

跑两遍,先数实例

如果同一条日志在同一秒出现两次,带上 实例名、容器 ID 或主机名 再看一次。两个应用实例各自启动了 @Scheduled,就会各自执行;这不是 Spring 在一个实例里“重复触发”。多实例只允许运行一次的任务,需要分布式锁、独立调度服务,或只在单独的 worker 实例启用,不能靠单机 synchronized 解决。

若确认只有一个实例,检查是否把同一个任务类注册成了两个 Bean,或同一方法上重复声明了多个 @Scheduled。Spring 会分别处理重复的调度声明。排查时先观察 Bean 数量和触发日志,不要为了止住重复执行而直接删一半业务逻辑。

最后做一次贴近生产的复测

把探针恢复为真实 cron,在测试环境明确时区和启动 profile;检查预期时间前后各一次日志。任务若会写数据库或发消息,再做一次重复触发测试,确认幂等逻辑。日志本身找不到时,可以接着看Spring Boot 日志文件没生成怎么排查。

参考资料

Spring Framework 6.2:Task Execution and Scheduling
Spring Boot 3.5:Task Execution and Scheduling

更新记录

2026-10-28:创建文章,补充最小探针、时区、线程池与多实例排查。