Spring定时任务配置最快能跑起来的方式,是主类加@EnableScheduling、方法上加@Scheduled,三行代码搞定;但真正决定它能不能上生产的,是线程池、cron时区和分布式重复执行这三件事。

最小可用配置:十分钟跑通第一个定时任务
Spring把定时任务做得很轻,轻到很多人第一次用会怀疑是不是漏配了什么。
三步开启
- 启动类或任意@Configuration类上加
@EnableScheduling,这是总开关,不加的话所有@Scheduled方法都是摆设。 - 写一个@Component类,方法上加
@Scheduled。 - 确认方法无参、返回void,且所在Bean被Spring管理。
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Component
public class ReportJob {
@Scheduled(cron = "0 0 2 ?", zone = "Asia/Shanghai")
public void buildDailyReport() {
// 业务逻辑
}
}
zone这个属性建议显式写上,服务器时区是UTC时,你的凌晨两点任务会老老实实在北京时间上午十点跑,这类问题排查起来很费时间。
cron表达式怎么读
Spring的cron是六位(秒 分 时 日 月 周),支持七位(末尾加年),几个常用写法:
0 0 2 ?每天凌晨2点0 /5 ?每5分钟0 0 9 ? MON-FRI工作日早上9点0 30 1 1 ?每月1号1点30
只能出现在“日”和“周”其中一位,因为这两个字段互斥,写成这种六星形式,多数情况下会被Spring拒绝启动,报“cron表达式非法”。
Spring定时任务cron表达式和fixedRate到底有什么区别
这是新手最容易纠结的一组选择,选错了轻则任务堆积,重则把数据库打满。
四种调度方式的真实差异
| 方式 | 触发规则 | 适用场景 | 要注意的点 |
|---|---|---|---|
cron |
按日历时间点触发 | 日报、对账、清理类任务 | 时区必须显式指定 |
fixedRate |
按固定频率触发,从上一次开始时间算 | 心跳、轮询 | 任务耗时长会重叠 |
fixedDelay |
上一次结束后延迟固定时间 | 串行处理队列 | 周期会被拉长 |
initialDelay |
首次延迟,配合上面三者 | 避免启动瞬间打满 | 常和fixedDelay搭配 |
关键区别在参照点:fixedRate盯着“上一次开始时间”,fixedDelay盯着“上一次结束时间”,一个任务平均跑8秒,你配了fixedRate=5000,那它永远在追赶,线程池里会堆任务,改成fixedDelay=5000,节奏立刻稳下来。
业内专家指出,定时任务的频率配置本质是“业务能容忍多晚”和“系统能承受多快”之间的平衡,而不是技术参数的堆砌。
选型的一句话原则
- 业务关心“几点执行”,用cron。
- 业务关心“隔多久执行一次”,用fixedDelay。
- 只有在明确知道任务执行时间远小于间隔时,才用fixedRate。
Spring Boot定时任务配置多线程怎么开启
这个问题问得很多,因为默认行为确实容易踩坑。
默认只有单线程
不配置的话,Spring会用一个单线程的调度器跑所有@Scheduled方法,两个任务都定在整点,第二个必须等第一个跑完,你以为是并行,实际是排队。
验证方法很简单:写两个各睡眠3秒的任务,配同一个cron,观察日志时间戳是否重叠。

两种开启方式
方式一,Spring Boot 2.1之后最省事,配置文件两行:
spring.task.scheduling.pool.size=8 spring.task.scheduling.thread-name-prefix=sched-
方式二,需要精细控制优雅停机时,用代码配置:
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(8);
scheduler.setThreadNamePrefix("sched-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
scheduler.initialize();
registrar.setTaskScheduler(scheduler);
}
}
线程池大小别拍脑袋,多数情况下,池子大小等于“同一时刻可能触发的任务数”,再加上一点冗余就够了,开到几十上百,只是把压力转移到下游数据库。
setWaitForTasksToCompleteOnShutdown(true)这一行很值得加,否则应用停机时正在跑的任务会被直接掐断,跑到一半的批量更新就成了脏数据。
Spring定时任务分布式部署重复执行怎么解决
单机时代没人关心这个问题,一旦部署两个实例,同一个凌晨对账任务就跑了两次,账目直接翻倍。
四种主流方案的取舍
- ShedLock:加一个数据库表存锁记录,注解式使用,侵入小,适合实例数不多、已有数据库的中小型项目。
- Redis分布式锁:用
SET key value NX PX 30000抢锁,性能好,要注意锁续期和释放的原子性,不然会出死锁。 - XXL-JOB、PowerJob这类调度中心:把调度权收归中心,执行器只负责跑,适合任务多、需要可视化管理、失败重试和告警的团队。
- 云厂商托管调度:按量计费,省运维,长三角和珠三角不少团队在业务快速扩张期会选这条路,代价是绑定了平台。
成本这笔账要算清楚
选型时绕不开成本,自建调度中心省下的是订阅费和云服务账单,花掉的是运维人力、机器资源和故障响应时间,托管服务反过来,行业共识认为,任务数量在几十个以内、团队没有专职中间件运维时,托管或轻量锁方案的总体成本更低;任务上百、调度逻辑复杂时,自建调度中心的长期收益更明显。
如果是在评估外包报价,注意区分“只配几个@Scheduled”和“要接入分布式调度并做监控告警”,两者工作量差好几倍,报价自然不在一个档位。
ShedLock的最小示例
@Scheduled(cron = "0 0 2 ?")
@SchedulerLock(name = "buildDailyReport",
lockAtMostFor = "30m", lockAtLeastFor = "1m")
public void buildDailyReport() {
// 同一时刻只会有一个实例进入
}
lockAtMostFor是兜底,防止持锁实例宕机后锁永不释放。lockAtLeastFor是防抖,避免任务执行过快导致多实例在锁间隙里轮流进入。
生产环境绕不开的几个实操细节
配置跑通只是起点,下面这些才是事故高发区。
异常必须自己兜住
@Scheduled方法抛异常,Spring默认只打一行日志,任务下次照跑,但这次的数据就丢了,建议在方法体内部try-catch,把失败任务落到补偿表,配一个重试任务扫表。

别让定时任务和业务抢连接池
凌晨批量任务一开,数据库连接瞬间打满,早高峰的用户请求跟着超时,给批量任务单独配数据源,或者限制单次处理条数、分批提交,是很常见的做法。
加一条执行日志
至少记录任务名、开始时间、结束时间、处理条数、是否成功,出问题时这几列信息比任何监控面板都直接。
优雅停机要演练
配置了等待时间不等于万事大吉,上线前手动kill一次进程,看看正在跑的任务是正常收尾还是被截断,比看文档靠谱。
写在最后
Spring定时任务配置的门槛很低,@EnableScheduling加@Scheduled就能跑;但把它放进生产环境的门槛不低,线程池、时区、幂等、异常、停机这五件事一件都不能省,先把最小配置跑通,再按业务量决定要不要上分布式调度,这个顺序比一上来就堆中间件更省事。
关于Spring定时任务配置的常见疑问
Spring定时任务配置后为什么不执行
按顺序排查四处:启动类有没有@EnableScheduling;任务类是否被Spring扫描到;cron表达式是否合法(六位或七位,的位置对不对);方法是否为public void且无参,还有一个隐蔽原因类被自己new出来了,没走容器,自然不生效。
Spring定时任务和Quartz、XXL-JOB怎么选
只做单机、任务数量少、不需要失败重试和可视化管理,用Spring自带的@Scheduled足够,需要持久化、集群、任务依赖编排,选Quartz或调度中心,判断标准是任务数量和管理诉求,不是框架新旧。
定时任务的时间不准是怎么回事
先看服务器时区和JVM时区是否一致,再看任务本身耗时是否超过了触发间隔导致排队,用fixedRate配短间隔跑长任务时,延迟会越积越多,改用fixedDelay或者扩大线程池都能缓解。
