@Scheduled(cron = "0 /5 ?") 执行原理详解
- Cron 表达式解析
"0 */5 * * * ?"
│ │ │ │ │ └─ 星期几(?表示不指定)
│ │ │ │ └─── 月份(*表示每月)
│ │ │ └───── 日期(*表示每日)
│ │ └─────── 小时(表示每小时)
│ └────────── 分钟( /5表示每5分钟)
└───────────── 秒(0表示整秒触发)
触发时间示例:
• 14:00:00
• 14:05:00
• 14:10:00
• 14:15:00
• ...
- 执行原理时序图
Spring 应用启动
↓
@EnableScheduling 扫描 @Scheduled 注解
↓
注册 CronTask 到 TaskScheduler
↓
TaskScheduler 内部线程定时检查 cron 表达式
↓
匹配时间 → 触发任务执行
- 线程行为分析
默认配置(单线程)
// Spring 默认创建单线程调度器
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1); // 默认只有1个线程!
执行情况:
时间线:
14:00:00 - 线程A开始执行task3()
14:02:00 - task3()还在执行
14:05:00 - cron时间到,但线程A被占用,task3()排队等待
14:07:00 - 线程A完成任务 → 立即执行排队的task3()
问题:任务会排队堆积,实际执行间隔不是严格的5分钟!
自定义线程池配置
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 多线程
return scheduler;
}
}
执行情况:
时间线:
14:00:00 - 线程A执行task3()
14:05:00 - 线程B执行task3()(并行执行)
14:10:00 - 线程C执行task3()(并行执行)
- 与其他调度方法的区别
调度方式 触发时机 线程行为 适用场景
fixedDelay 上次任务结束后固定时间 严格不重叠 数据同步
fixedRate 固定频率,不等待完成 可能重叠 监控任务
cron 绝对时间点 可能排队/重叠 定时报表
- 实际代码演示
@Component
@Slf4j
public class CronTaskDemo {
private final AtomicInteger executionCount = new AtomicInteger(0);
@Scheduled(cron = "0 */5 * * * ?") // 每5分钟整点执行
public void task3() {
int count = executionCount.incrementAndGet();
String threadName = Thread.currentThread().getName();
long startTime = System.currentTimeMillis();
log.info("=== 第{}次执行开始 [线程:{}] ===", count, threadName);
// 模拟任务执行(假设需要2分钟)
try {
Thread.sleep(120000); // 2分钟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long cost = System.currentTimeMillis() - startTime;
log.info("=== 第{}次执行结束 [耗时:{}ms] ===", count, cost);
}
}
运行结果(默认单线程):
14:00:00 - === 第1次执行开始 [线程:pool-1-thread-1] ===
14:02:00 - === 第1次执行结束 [耗时:120000ms] ===
14:05:00 - === 第2次执行开始 [线程:pool-1-thread-1] === ← 准时触发
14:07:00 - === 第2次执行结束 [耗时:120000ms] ===
运行结果(任务耗时超过5分钟的情况):
14:00:00 - === 第1次执行开始 [线程:pool-1-thread-1] ===
14:05:00 - cron时间到,但线程被占用,任务排队
14:07:00 - === 第1次执行结束 [耗时:420000ms] ===
14:07:00 - === 第2次执行开始 [线程:pool-1-thread-1] === ← 立即执行排队任务
- 数据同步场景的特殊考虑
风险:任务执行时间 > 5分钟
@Scheduled(cron = "0 */5 * * * ?")
public void dataSync() {
// 总耗时可能超过5分钟!
}
解决方案
@Component
@Slf4j
public class SafeCronDataSync {
private final AtomicBoolean isRunning = new AtomicBoolean(false);
@Scheduled(cron = "0 */5 * * * ?")
public void dataSync() {
// 防止任务重叠
if (!isRunning.compareAndSet(false, true)) {
log.warn("前一个数据同步任务仍在执行,跳过本次cron触发");
return;
}
try {
// 实际数据同步逻辑
doDataSync();
} finally {
isRunning.set(false);
}
}
private void doDataSync() {
long start = System.currentTimeMillis();
// 1. 查询数据库
List<Map<String, Object>> data = queryData();
// 2. 异步发送Kafka(不阻塞cron线程)
CompletableFuture.runAsync(() -> {
sendToKafka(data);
});
long cost = System.currentTimeMillis() - start;
log.info("数据同步任务完成,耗时: {}ms", cost);
}
}
-
最佳实践总结
-
cron适合按绝对时间点执行,如整点报表
-
数据同步推荐使用fixedDelay,避免时间偏差累积
-
必须配置多线程池,避免任务排队
-
必须添加防重叠机制,防止数据重复处理
-
Kafka发送要用异步,不阻塞调度线程
对于你的场景,推荐:
@Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 10000) // 更合适
public void dataSync() {
// 防重叠 + 异步发送
}
@Scheduled(cron = "0 */5 * * * ?") 在任务执行时间可控(<3分钟)且需要严格按时间点执行时可以使用,但需要做好防重叠和线程池配置。