@Scheduled 定时任务不执行?八成是这 4 个问题
导读
求职招聘系统里定时任务不少:简历解析完重试、职位过期自动下架、零点结算日结工资。Spring Boot 的 @Scheduled 看着简单,加个注解就完事,但线上不执行、重复执行、执行一半挂掉,都是坑。这篇把我踩过的坑整理下。

坑一:默认单线程,任务互相堵死
这是最隐蔽的坑。Spring 的 @Scheduled 默认只有一个线程,所有定时任务共用。你的任务 A 跑 10 秒,任务 B 到点了也得排队等 A 跑完。
我遇到过一次:职位过期下架任务里调了个慢接口,阻塞了 30 秒,结果零点工资结算任务跟着延迟了 30 秒,用户投诉"工资没到账"。
解法:配置线程池。
java
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}
每个任务各跑各的,互不干扰。
坑二:cron 表达式写错,任务永远不触发
cron 是 6 位(秒 分 时 日 月 周),跟 Linux 的 5 位不一样。我刚开始写 0 0 0 * * * 想每天零点执行,结果发现是 5 位,Spring 直接启动报错,但报错在日志里很不显眼,你不翻日志根本不知道任务没注册上。
正确写法:
java
@Scheduled(cron = "0 0 0 * * ?") // 每天 0 点
@Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟
周字段用 ? 不用 *,这是新手最容易错的地方。
坑三:集群部署任务重复执行
本地开发没事,一上集群,每个节点都在跑定时任务,工资结算执行了 3 次,用户收到 3 次到账通知。这问题必须解决。
方案一是用分布式锁(Redis):
java
@Scheduled(cron = "0 0 0 * * ?")
public void dailySettle() {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:dailySettle", "1", Duration.ofMinutes(30));
if (!Boolean.TRUE.equals(locked)) {
log.info("其他节点正在执行,跳过");
return;
}
try {
// 业务逻辑
} finally {
redisTemplate.delete("lock:dailySettle");
}
}
方案二是让定时任务只在一个节点跑(配个开关,只有一台机器开 @Scheduled)。我后来用了 Redis 锁,因为配置开关容易忘,锁更保险。
坑四:任务内部异常,不打印就静默
@Scheduled 方法里抛异常,如果方法没 try-catch,日志默认只打一行 ERROR,堆栈还不全,排查全靠猜。更坑的是,异常不影响下一次触发,所以任务会"看起来在跑,实际每次都失败"。
我的习惯:
java
@Scheduled(cron = "0 0 2 * * ?")
public void retryResumeParse() {
log.info("简历解析重试任务开始");
try {
doRetry();
} catch (Exception e) {
log.error("简历解析重试任务失败", e); // 堆栈打全
}
}
而且重试任务我还会给每条数据记状态(重试次数、下次重试时间),而不是整个任务一把梭,这样单条失败不会影响其他数据。
总结
@Scheduled 的坑基本都是"看着没跑,其实跑了"或者"跑了没结果"。配线程池、写对 cron、加分布式锁、内部 try-catch 打全日志,这四件事做完,定时任务基本就稳了。现在我做定时任务,第一件事就是先写 cron 验证工具确认表达式没错,再往里填业务。