本文适合所有写过
@Scheduled
的朋友阅读,写完一个定时任务,别急着部署,看完这篇文章你可能会救下自家的生产服务器。
一、故事从一个人畜无害的注解开始
你可能也经历过这样的场景:
"这个任务每10秒执行一次,查一下数据库就行,很简单。"
于是你信心满满地写下了:
java
@Scheduled(fixedRate = 10000)
public void checkOrderStatus() {
List<Order> orders = orderService.getPendingOrders();
for (Order order : orders) {
orderService.syncOrderStatus(order);
}
}
你写完后一拍大腿:我真是个天才!
结果上线3分钟后,数据库报警、CPU飙高、服务超时、老板电话响了,你还没来得及捂住耳朵。

二、定时任务的"阴间设计"现场
表面上这个任务简单到不能再简单,实际它踩的坑,一个比一个致命:
1. fixedRate
是啥意思你真的理解了吗?
@Scheduled(fixedRate = 10000)
的意思是:任务开始后,过 10 秒再触发下一次 ,不管上一次有没有执行完。
也就是说,如果 checkOrderStatus()
执行了 15 秒,那下一个任务会在第 10 秒强行开始。这就像两个汉堡没吃完,第三个已经送来了。
后果:线程堆积,任务重叠执行,争抢资源,服务器爆炸💣。
正确姿势:
java
@Scheduled(fixedDelay = 10000)
它的意思是:上次执行完之后再等10秒,再执行下一次。
如果你还想更稳妥一点,建议用 ScheduledExecutorService
或加个分布式锁(后面详细讲)。
2. 没有线程池配置 = 自杀式多线程爆破
Spring 默认的 @Scheduled
背后是 TaskScheduler
,没配置线程池的话,它就用单线程跑所有任务。
如果你有多个 @Scheduled
,它们会排队执行,一旦某个任务执行太慢,其他任务就开始挤牙膏。
你以为它们在"并发执行",其实它们在"排队自杀"。
正确姿势:
java
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 根据业务合理配置
scheduler.setThreadNamePrefix("my-task-");
scheduler.initialize();
return scheduler;
}
}
3. 日志打得飞起?磁盘表示已卒
很多人为了调试方便,会在定时任务里狂打日志:
java
log.info("正在同步订单:{}", order.getId());
同步 10 万个订单,每次执行一次,生成 10 万条日志。
后果:日志爆炸,磁盘爆满,甚至影响其他服务写日志。
正确姿势:
- 打点式日志:只记录关键指标,比如每轮执行的时间、处理条数、异常数。
- 加限频器:用布隆过滤器、计数器等手段,只记录代表性异常。
- 日志分级写到不同的文件,Info 和 Error 分离。
三、如果你还在分布式环境下......
那事情就更"有趣"了。
你有两个节点都部署了这个服务,每个节点都跑了一遍定时任务,然后它们在数据库里"撞单子",你后台发起的请求都被"并发插入"冲爆。
这时候你终于意识到------我不是在写任务,我是在模拟DDoS攻击。
正确姿势:加锁!
基于 Redis 的分布式锁(推荐)
java
boolean locked = redisLock.tryLock("sync_order", 30);
if (locked) {
try {
// 你的逻辑
} finally {
redisLock.unlock("sync_order");
}
}
或者你用数据库实现也可以,每次执行任务前插一条对应的记录,并且用执行批次来作唯一索引,如果插入成功就执行任务,插入失败就跳过执行,当然具体情况还得视项目而定。
四、任务太慢怎么办?排队还是并发?
如果任务耗时特别长,考虑以下优化方式:
拆成多个小任务异步处理
别在定时任务里干重活儿,而是只负责"发号施令"。
java
for (Order order : orders) {
executorService.submit(() -> orderService.syncOrderStatus(order));
}
结合线程池、消息队列甚至事件驱动,定时任务可以干得更轻松。
加监控,别做"盲盒任务"
定时任务不报错不代表没问题,你需要:
- 记录耗时、处理数量、失败数量
- 打点到 Prometheus / Grafana
- 加报警:比如连续执行失败3次就钉钉/企微报警
五、最后来点警句
"定时任务不出问题,是因为你还没上线。"
"定时任务不该做太多事,它只是个闹钟,不是个保姆。"
"没有监控的定时任务,就像睁眼走夜路。"
"默认配置的
@Scheduled
,是定时埋雷的第一步。"
六、总结
定时任务就像厨房里的燃气灶,用得好能煮饭,用不好就把房子烧了。看似简单的活,实则暗藏杀机。
一起来回顾一下关键点:
误区 | 正确姿势 |
---|---|
使用 fixedRate ,忽略任务耗时 |
考虑 fixedDelay 或自行控制调度逻辑 |
不配线程池,任务串行排队 | 配置合适的 TaskScheduler 线程池 |
每个节点都执行任务 | 加 Redis 锁或指定主节点 |
日志无限输出 | 控制日志量、打点记录、异常聚合 |
没有监控 | 添加任务运行指标,设立报警机制 |
写定时任务不是问题,问题是你把它当成"定时执行的 main 方法"来用。
别让一个看似温柔的注解,成为你下一个线上事故的罪魁祸首。
