在后端开发中,定时任务属于刚需基础组件:订单超时自动关单、优惠券定时失效、日志/垃圾文件定时清理、统计报表凌晨生成、缓存定时刷新、分布式数据同步……几乎所有中后台项目都会用到。
SpringBoot 自带的定时任务方案开箱即用,无需引入中间件(如 Quartz、XXL-Job),轻量化、无依赖。
一、定时任务整体架构与适用场景
核心分类
1. 静态定时任务
- 基于 @Scheduled 实现
- 规则写死在代码或配置文件,启动后不可修改
- 优点:简单、零依赖、开发极快
- 缺点:不灵活、单线程易阻塞
2. 动态定时任务
- 基于 SchedulingConfigurer + Trigger 实现
- 运行时可修改 cron、启停任务,无需重启服务
- 优点:高度灵活,支持运营后台配置
- 缺点:代码稍复杂,需要自己维护任务状态
3. 分布式定时任务
- 如 Quartz、XXL-Job、Elastic-Job
- 解决集群下任务重复执行问题
- 本文重点讲 Spring 内置方案,分布式仅作对比
典型业务场景
- 每日凌晨:2:00 清理 7 天前日志、生成日账单
- 超时任务:下单 30 分钟未支付自动取消
- 周期巡检:定时检查服务器状态、接口健康度
- 动态配置:运营在后台修改任务执行时间
二、@Scheduled 静态定时任务
1. 开启定时任务总开关
启动类添加 @EnableScheduling,否则定时任务不生效:
- 如 Quartz、XXL-Job、Elastic-Job
- 解决集群下任务重复执行问题
- 本文重点讲 Spring 内置方案,分布式仅作对比
- 固定周期:每 5 分钟刷新一次统计数据
- 每日凌晨:2:00 清理 7 天前日志、生成日账单
- 超时任务:下单 30 分钟未支付自动取消
- 周期巡检:定时检查服务器状态、接口健康度
- 动态配置:运营在后台修改任务执行时间
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 开启 Spring 定时任务
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. @Scheduled 三大核心参数
@Scheduled 共支持 3 种触发规则,不可同时混用,只能选其一。
(1)fixedRate:固定频率执行
- 含义:从上一次任务开始时间计算,每隔指定毫秒执行一次
- 任务执行时间 > 间隔时间 → 任务会排队,不会并行
// 每 5 秒执行一次(从上一次启动时算)
@Scheduled(fixedRate = 5000)
public void taskFixedRate() {
System.out.println("fixedRate 每5秒执行:" + LocalDateTime.now());
}
(2)fixedDelay:固定延迟执行
- 含义:从上一次任务执行结束时间计算,等待指定毫秒再执行
- 保证任务串行,不会重叠
// 上一次执行完,等 5 秒再执行
@Scheduled(fixedDelay = 5000)
public void taskFixedDelay() {
System.out.println("fixedDelay 执行完延迟5秒:" + LocalDateTime.now());
}
(3)cron:Cron 表达式
- 支持秒级复杂规则:每天、每周、每月、指定时分秒
- 格式:
秒 分 时 日 月 周
// 每 10 秒执行一次
@Scheduled(cron = "0/10 * * * * ?")
public void taskCron() {
System.out.println("cron 每10秒执行:" + LocalDateTime.now());
}
3. 初始延迟:initialDelay
项目启动后,不立即执行,等待一段时间再开始:
// 项目启动延迟 3 秒后,每 5 秒执行一次
@Scheduled(initialDelay = 3000, fixedRate = 5000)
public void taskInitialDelay() {
System.out.println("启动延迟3秒后执行");
}
4. Cron 表达式完整详解
语法结构:
秒(0-59) 分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(1-7,1=周日,7=周六)
常用符号:
*:每一秒/每一分/每一时刻?:不指定(日和周互斥,必须有一个为 ?)/:步长,如0/5表示从 0 开始,每 5 单位-:范围,如10-20表示 10 到 20,:枚举,如1,3,5表示 1、3、5
示例:
"0 0 2 * * ?" = 每天凌晨 2 点 "0 0 0 * * ?" = 每天午夜 0 点 "0 0/5 * * * ?" = 每 5 分钟 "0 0 12 * * ?" = 每天中午 12 点 "0 0 8 ? * MON-FRI" = 工作日早上 8 点 "0 0 0 1 * ?" = 每月 1 号零点 "0/1 * * * * ?" = 每秒执行
5. 从配置文件读取
硬编码 cron 不利于维护,推荐放到 application.yml:
# 定时任务配置
scheduled:
task1:
cron: "0/5 * * * * ?"
task2:
fixed-rate: 10000
initial-delay: 5000
使用:
@Scheduled(cron = "${scheduled.task1.cron}")
public void configTask() {
System.out.println("从yml读取cron执行");
}
三、解决单线程阻塞问题
Spring 定时任务默认是单线程,多个任务会排队,一个卡死全部卡住。
1. 配置定时任务线程池
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@EnableScheduling
public class ScheduledThreadPoolConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 核心线程数,根据任务数量设置
scheduler.setPoolSize(10);
// 线程名称前缀,方便日志排查
scheduler.setThreadNamePrefix("scheduled-task-");
// 等待任务完成再关闭
scheduler.setWaitForTasksToCompleteOnShutdown(true);
// 关闭最大等待时间
scheduler.setAwaitTerminationSeconds(60);
scheduler.initialize();
return scheduler;
}
}
配置后,多个任务可并行执行,互不阻塞。
四、异步 + 定时(@Async + @Scheduled)
让定时任务异步执行,不占用调度线程,进一步提升稳定性。
1. 开启异步
启动类加 @EnableAsync:
@SpringBootApplication
@EnableScheduling
@EnableAsync // 开启异步
public class Application {}
2. 异步定时任务
@Component
public class AsyncScheduleTask {
@Async
@Scheduled(cron = "0/5 * * * * ?")
public void asyncTask() {
System.out.println("异步定时任务执行:" + Thread.currentThread().getName());
}
}
优点:
- 任务执行耗时不影响调度
- 异常不会导致调度线程崩溃
五、动态定时任务
@Scheduled 启动后不可改 cron,运营后台配置、动态调整必须用动态定时。
1. 核心原理
- 实现
SchedulingConfigurer - 重写
configureTasks注册任务 - 使用
CronTrigger动态读取最新 cron - 把 cron 存在 DB/Redis/Nacos 中,实现真正可配置
2. 完整动态定时任务实现
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
@Configuration
public class DynamicScheduleConfig implements SchedulingConfigurer {
// 动态 cron(真实项目从数据库/Redis读取)
private String cron = "0/5 * * * * ?";
// 任务启停开关
private final AtomicBoolean taskEnabled = new AtomicBoolean(true);
// 外部修改 cron
public void setCron(String cron) {
this.cron = cron;
log.info("动态修改cron成功:{}", cron);
}
// 启停任务
public void setTaskEnabled(boolean enabled) {
this.taskEnabled.set(enabled);
log.info("任务状态已修改:{}", enabled ? "开启" : "关闭");
}
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
// 注册动态任务
registrar.addTriggerTask(
// 任务业务逻辑
() -> {
if (!taskEnabled.get()) {
log.info("任务已关闭,跳过执行");
return;
}
log.info("动态定时任务执行:{}", LocalDateTime.now());
// 执行业务逻辑...
},
// 动态触发器:每次执行前重新获取cron
triggerContext -> {
CronTrigger cronTrigger = new CronTrigger(cron);
return cronTrigger.nextExecutionTime(triggerContext);
}
);
}
}
3. 提供接口动态控制
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/schedule")
public class ScheduleController {
@Autowired
private DynamicScheduleConfig dynamicScheduleConfig;
// 动态修改 cron
@GetMapping("/updateCron")
public String updateCron(@RequestParam String cron) {
try {
dynamicScheduleConfig.setCron(cron);
return "修改成功,新cron:" + cron;
} catch (Exception e) {
return "cron 格式非法:" + e.getMessage();
}
}
// 开启/关闭任务
@GetMapping("/enable")
public String enableTask(@RequestParam boolean enabled) {
dynamicScheduleConfig.setTaskEnabled(enabled);
return "任务已" + (enabled ? "开启" : "关闭");
}
}
测试接口:
# 修改为每10秒执行一次 http://localhost:8080/schedule/updateCron?cron=0/10 * * * * ? # 关闭任务 http://localhost:8080/schedule/enable?enabled=false
无需重启,立即生效!
六、定时任务异常处理
定时任务一旦抛异常且未捕获,会导致后续调度失效,必须做异常防护。
1. 统一 try-catch 防护
@Scheduled(cron = "${scheduled.task1.cron}")
public void safeTask() {
try {
// 业务逻辑
System.out.println("任务执行");
} catch (Exception e) {
// 记录日志 + 告警
log.error("定时任务执行异常", e);
}
}
2. 全局异常捕获
结合 AOP 或全局异常切面统一捕获,避免每个任务写 try-catch。
七、静态 vs 动态 方案对比
|
方案 |
优点 |
缺点 |
适用场景 |
|
@Scheduled 静态 |
极简、零依赖、上手快 |
不可修改、单线程阻塞 |
固定周期、不常变更的任务 |
|
动态定时(Trigger) |
运行可改、可启停、灵活配置 |
代码稍复杂 |
运营后台配置、动态调整、多环境适配 |
|
异步定时 |
不阻塞调度、高可用 |
需注意线程安全、事务 |
耗时任务、批量处理任务 |
八、注意事项
1. 禁止在定时任务中写超大耗时操作
- 超过 1 分钟的任务建议丢 MQ 或线程池异步处理。
2. 集群部署必须防止重复执行
Spring 内置定时不支持分布式,集群下会多节点同时执行。
解决方案:
- 数据库乐观锁
- Redis 分布式锁(Redisson)
- 使用 XXL-Job 分布式定时任务
3. 关键任务必须加监控告警
- 任务执行失败、超时,通过邮件/钉钉/企业微信告警。
4. cron 尽量避开高峰整点
- 大量任务都在 0 点执行会导致瞬间压力过大,错开执行。
5. 日志必须清晰
- 记录任务开始/结束时间、耗时、异常信息,方便排查。
九、总结
- 1.
@EnableScheduling是定时任务开关,缺一不可; - 2.
@Scheduled支持fixedRate/fixedDelay/cron,生产优先用配置化 cron; - 3. 多任务必须配置线程池,避免单线程阻塞;
- 4. 耗时任务用
@Async异步化,提升系统稳定性; - 5. 动态定时基于
SchedulingConfigurer,可实现运行时修改、启停; - 6. 生产务必做异常捕获、监控告警、分布式锁防重复。













