原文来自于:zha-ge.cn/java/128
定时任务写错姿势?@Scheduled 才是 Spring 官方推荐方案
有一次深夜上线项目,我盯着那段写了快 300 行的定时调度代码,陷入了沉思: 用 Timer
手搓线程、用 ScheduledExecutorService
手动调度、还得自己 try-catch、记日志、管异常......
写得我手都麻了,结果一上线还出错。领导看了眼日志:"兄弟,这不是 Spring 项目吗?你为啥不用 @Scheduled
?" 那一刻我意识到:我可能一直在用"上世纪"的方式写定时任务。
传统写法的痛点:能跑但不优雅
很多初学者写定时任务都是这么干的👇:
java
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
doTask();
}, 0, 10, TimeUnit.SECONDS);
或者更老一点的:
java
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
doTask();
}
}, 0, 10000);
这些写法没错,但问题一堆:
- ❌ 代码冗长:每次都得自己写线程池、调度逻辑
- ❌ 异常容易吞:一个异常就可能让整个任务线程挂掉
- ❌ 配置不统一:无法通过配置文件轻松调整调度周期
- ❌ 和 Spring 生命周期脱节:Bean 还没加载完就开始跑任务,容易出错
说白了,你在 Spring 项目里"手写轮子"了。
一行注解搞定:@Scheduled
登场!
Spring 从很早的版本就内置了定时任务调度器,最核心的就是一个注解:@Scheduled
。它让我们从"造轮子"解放出来,代码优雅得像 DSL:
java
@Component
public class ReportTask {
@Scheduled(fixedRate = 5000) // 每5秒执行一次
public void generateReport() {
System.out.println("📊 正在生成报表..." + LocalDateTime.now());
}
}
只要加上这两步,Spring 就能自动帮你定时调度这个方法:
-
在配置类上启用调度功能:
java@EnableScheduling @Configuration public class ScheduleConfig {}
-
给方法加上
@Scheduled
,Spring 会自动扫描并注册。
不用创建线程、不用写调度器、不用 try-catch,Spring 帮你全管了。
三种常用写法,灵活到飞起
1. fixedRate
------ 固定速率执行(从上次开始时间算起)
java
@Scheduled(fixedRate = 10000)
public void taskA() {
log.info("每 10 秒执行一次(fixedRate)");
}
👉 不管上次任务花了多久,10 秒到就再次执行(周期性强)。
2. fixedDelay
------ 固定延迟执行(从上次结束时间算起)
java
@Scheduled(fixedDelay = 5000)
public void taskB() {
log.info("上次结束后 5 秒再执行(fixedDelay)");
}
👉 上次执行完 → 等 5 秒 → 再执行,常用于串行任务。
3. cron
------ 类 Unix 风格表达式,精准调度
java
@Scheduled(cron = "0 0/5 * * * ?")
public void taskC() {
log.info("每 5 分钟执行一次(cron)");
}
cron
是最强大的:
"0 0 12 * * ?"
→ 每天中午 12 点执行"0 */30 9-18 * * MON-FRI"
→ 工作日 9 点到 18 点每 30 分钟一次
📌 小技巧:推荐用 cron 在线生成器 帮你校验表达式。
高级用法:@Scheduled 不只是"定时"
1. 配合配置文件,动态控制任务周期
java
@Scheduled(fixedRateString = "${task.report.interval:5000}")
public void generateReport() { ... }
👉 在 application.yml
中改:
yaml
task:
report:
interval: 10000
不改代码,就能调整任务频率。
2. 搭配 @Async
异步执行
默认情况下,@Scheduled
任务是串行执行的。如果你有多个任务不想互相阻塞,可以这样:
java
@Async
@Scheduled(fixedRate = 5000)
public void asyncTask() {
log.info("异步执行任务:" + Thread.currentThread().getName());
}
⚠️ 记得在配置类上加 @EnableAsync
。
3. 分布式下防止"多节点重复执行"
在多节点环境下,多个实例可能会重复跑同一个任务。这时推荐使用分布式任务调度框架(如 Quartz、XXL-Job、ElasticJob),或者手动加分布式锁(比如 Redis Lock):
java
@Scheduled(cron = "0 0 * * * ?")
public void safeTask() {
if (redisLock.tryLock("task-lock", 30)) {
doTask();
}
}
常见踩坑与排雷指南
坑点 | 症状 | 解决方案 |
---|---|---|
忘记 @EnableScheduling |
方法不执行 | 在配置类加上它 |
方法不是 public |
不触发 | @Scheduled 方法必须是 public |
抛异常任务停止 | 任务只跑一次 | try-catch 包裹任务逻辑 |
多个任务互相阻塞 | 部分任务延迟甚至不执行 | 配合 @Async 或自定义 TaskScheduler |
多实例重复执行 | 数据重复写入 | 使用分布式锁或分布式调度框架 |
面试必备:@Scheduled 和 Quartz 有啥区别?
面试官很喜欢问这个:
@Scheduled
:轻量级、内置于 Spring 容器,简单任务、单机任务的首选。- Quartz:重量级、支持分布式调度、任务持久化、错过补偿、集群协调,适合大型调度平台。
一句话总结: 👉 @Scheduled
更像"闹钟"------准时响; 👉 Quartz 像"排班系统"------管理复杂任务。
总结:会写和会用,是两回事
很多人都"会写"定时任务,但未必"写得优雅":
- ✅ 用
@Scheduled
取代手动线程管理,让调度与 Spring 生命周期融为一体 - ✅ 用
fixedRate
、fixedDelay
、cron
灵活适配各种业务场景 - ✅ 配合配置文件、异步、分布式锁,让任务真正上生产级
一句话记住:
"你可以不用
@Scheduled
,但你错过的不是一行注解,而是一整套官方推荐的调度体系。"