大家好,我是加洛斯。作为一名程序员👨💻,我深深信奉费曼学习法------教,是最好的学 📚。这里是我的知识笔记与分享,旨在把复杂的东西讲明白。如果发现有误🔍,万分欢迎你帮我指出来!废话不多说,正文开始 👇
一、基础知识
Spring Task 是 Spring 框架提供的任务调度模块,用于实现定时任务功能。它是 Spring3.0 后引入的,基于注解和配置的方式,简化了任务调度的实现。
- 轻量级:无需引入额外的调度框架(如 Quartz)
- 注解驱动 :通过
@Scheduled注解即可定义定时任务 - 集成简单:与 Spring 框架无缝集成
- 支持 Cron 表达式:提供灵活的定时规则定义
Spring Task的依赖是org.springframework.boot,这个在我们创建项目的时候就存在的,一般情况下来讲我们不需要额外引入,但是以防万一我们还是在pom文件中查找一下是否存在。
xml
<!-- Spring Boot 项目依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
启用定时任务:想要开启定时任务我们需要在main方法中添加注解@EnableScheduling。
java
@SpringBootApplication
@EnableScheduling // 开启定时任务支持
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
我们来简单的举个小例子来讲述一下,看这个例子当中我们是不是有很多注解不明白?我们来详细的讲一下。
java
@Component
public class TaskTest {
// 固定频率执行,每5秒执行一次
@Scheduled(fixedRate = 5000)
public void executeFixedRate() {
System.out.println("固定频率任务执行: " + new Date());
}
// 固定延迟执行,上次执行完成后延迟3秒执行
@Scheduled(fixedDelay = 3000)
public void executeFixedDelay() {
System.out.println("固定延迟任务执行: " + new Date());
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 初始延迟2秒后开始执行,然后每10秒执行一次
@Scheduled(initialDelay = 2000, fixedRate = 10000)
public void executeWithInitialDelay() {
System.out.println("带初始延迟的任务执行: " + new Date());
}
}

二、@Scheduled注解参数详解
@Scheduled是 Spring Framework 3.0 引入的注解,用于声明一个方法是定时任务方法。这是 Spring Task 调度的核心注解。
java
//基本使用
@Component
public class MyScheduledTasks {
@Scheduled(fixedRate = 5000)
public void myTask() {
// 任务逻辑
}
}
2.1 fixedRate(固定频率执行)
作用:以固定频率执行任务,不管上一次任务是否完成。
- 固定速率:无论上次任务是否完成,到时间就会启动新执行
- 相对开始时间 :从上次开始执行的时间点 计算下一次执行,上一次任务开始执行后,间隔指定时间就会触发下一次执行
- 独立线程:默认使用单线程,可能发生任务重叠
@Scheduled(fixedRate = ) 后面可以接受多种类型的值,最常见的是long类型的毫秒值与TimeUnit时间单位(Spring 5.3+)
java
@Component
@Slf4j
public class FixedRateExample {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 每1秒执行一次
*/
@Scheduled(fixedRate = 1000)
public void fixedRateTask() {
int count = counter.incrementAndGet();
log.info("fixedRate 任务开始执行,第{}次", count);
try {
// 模拟任务执行时间
Thread.sleep(5000); // 任务执行需要5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("fixedRate 任务结束执行,第{}次", count);
}
}

观察上述代码,我们可以看到,这段代码并没有并发执行 这是为什么呢?
这是因为 Spring 默认使用单线程的线程池执行定时任务。
- @Scheduled 默认行为 :
- Spring 的
@Scheduled默认使用ThreadPoolTaskScheduler,默认配置下只有一个线程- 默认线程池名称是
scheduling-1(从日志中可以看到)- fixedRate 的原理 :
fixedRate = 1000意味着 每隔1秒应该开始一次新的执行- 但是如果前一个任务还在执行,且没有可用线程,新任务会在队列中等待
- 在日志中,任务都是串行执行的,因为只有一个线程
如果你确定这段代码可以并且需要并发执行,那么有多种方案可以解决当前的问题。我们最推荐的就是使用异步注解。
java
@Component
@Slf4j
@EnableAsync // 启用异步支持
public class TaskTest {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 每1秒执行一次
*/
@Async // 添加异步注解
@Scheduled(fixedRate = 1000)
public void fixedRateTask() {
int count = counter.incrementAndGet();
log.info("fixedRate 任务开始执行,第{}次", count);
try {
// 模拟任务执行时间
Thread.sleep(5000); // 任务执行需要5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("fixedRate 任务结束执行,第{}次", count);
}
}

可以看到启动异步注解后,我们的定时任务并发执行了。但是有一个问题,
@Async注解本身不会固定开启几个线程,它的行为取决于你如何配置线程池。在这种情况下在这种情况下:
- Spring 会使用
SimpleAsyncTaskExecutor- 每个异步任务都会创建一个新线程(无限制)
- 线程不会重用(执行完就销毁)
- 这可能会导致创建大量线程,有内存泄露风险
所以我们需要配置线程池 控制线程数量
yaml
spring:
task:
execution:
pool:
core-size: 3 # 核心线程数
max-size: 5 # 最大线程数
keep-alive: 60s # 空闲线程存活时间
thread-name-prefix: async-
以防有人不知道多线程策略,这里简单讲一下
任务提交顺序:
- 先使用核心线程(corePoolSize)
- 核心线程满了 → 放入队列(queueCapacity)
- 队列满了 → 创建新线程直到最大线程数(maxPoolSize)
- 都满了 → 执行拒绝策略
示例配置(core=5, max=10, queue=25):
- 同时处理5个任务 → 5个核心线程
- 又来第6-30个任务 → 放入队列(25个)
- 又来第31-40个任务 → 创建新线程(总共10个线程)
- 再来任务 → 执行拒绝策略

这样看我们最多只启用了三个线程,线程过少可能也会导致线程阻塞,所以我们需要根据实际情况进行修改。
2.2 fixedDelay(固定延迟执行)
作用:上一次任务执行完成后,延迟指定时间再执行下一次。
- 保证任务串行执行
- 不会出现并发执行的情况
java
@Component
@Slf4j
public class FixedDelayExample {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 上次任务执行完成后,延迟3秒再执行
*/
@Scheduled(fixedDelay = 3000)
public void fixedDelayTask() {
int count = counter.incrementAndGet();
log.info("fixedDelay 任务开始执行,第{}次", count);
try {
// 模拟任务执行时间
Thread.sleep(5000); // 任务执行需要5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("fixedDelay 任务结束执行,第{}次", count);
}
}
2.3 initialDelay(初始延迟)
作用:应用启动后,延迟指定时间再开始执行第一次任务。
- 通常与 fixedRate 或 fixedDelay 配合使用
- 避免应用启动时立即执行所有任务
java
@Component
@Slf4j
@EnableAsync // 启用异步支持
public class TaskTest {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 应用启动30秒后开始执行,之后每10秒执行一次
*/
@Scheduled(initialDelay = 30000, fixedRate = 10000)
public void taskWithInitialDelay() {
log.info("带初始延迟的任务执行,当前时间: {}",
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
}
}
2.4 Cron 表达式
Cron 表达式是一个字符串,由 6 或 7 个字段组成,用于定义定时任务的执行时间。
注意:Spring中的Cron表达式与标准Unix Cron的主要区别:
- 秒字段:Spring支持秒级精度(第一位),标准Cron没有
- 星期几:Spring中1-7表示周一到周日(或MON-SUN),0和7表示周日
java
// 每分钟的第30秒执行
@Scheduled(cron = "30 * * * * *")
public void everyMinuteAt30() {
log.info("每分钟的第30秒执行");
}
我们可以看到cron后面接了一堆***,这又是什么意思呢?我们看下面的图 
特殊字符说明
| 字符 | 含义 | 示例 | 说明 |
|---|---|---|---|
| * | 任意值 | * * * * * * |
每秒执行 |
| ? | 不指定值 | 0 0 12 * * ? |
仅用于日和星期字段 |
| - | 范围 | 0 0 10-12 * * * |
10点、11点、12点 |
| , | 多个值 | 0 0 10,14,16 * * * |
10点、14点、16点 |
| / | 步长 | 0 0/5 * * * * |
每5分钟 |
| L | 最后 | 0 0 0 L * ? |
每月最后一天 |
| W | 工作日 | 0 0 0 15W * ? |
最近的工作日 |
| # | 第几个星期几 | 0 0 0 ? * 2#3 |
每月第3个周一 |
不指定值:这对应表达式中的?问号。在 Cron 中,?用于 "不关心"或"无特定值" 的字段。它只能用在你不想指定具体值的字段上。仅用于日和星期字段:这是一个非常重要的规则说明 。它告诉你?这个符号只能出现在"日期(日)"字段和"星期几"字段中。
为什么有这个规则?
因为在 Cron 表达式的逻辑中, "日期(日)"和"星期几"本质上表达的是同一种概念(一个月的哪一天) 。如果你同时指定了"每月15号"和"每个星期二",调度器就会困惑:你到底是想在"每月15号"运行,还是"每个星期二"运行,还是"既是15号又是星期二"才运行?
为了消除这种歧义,Quartz 规定:
-
在星期字段使用
*实际上是允许的,它被视为"不指定具体星期几" -
真正的限制是:不能同时指定具体的日和具体的星期
- 错误:
* * * 15 * MON(指定了15号和周一) - 正确:
* * * 15 * ?或* * * ? * MON - 可以 :
* * * * * *(日和星期都是*,这是允许的)
- 错误:
这些个表达式认识就好,只要不是关小黑屋那种的开发就没必须死磕,现在ai这么发达,直接把你想要的发给ai生成或者解析就行。
来看一堆例子
java
@Component
@Slf4j
public class CronExample {
/**
* 基本Cron表达式示例
*/
// 每5秒执行一次
@Scheduled(cron = "*/5 * * * * *")
public void everyFiveSeconds() {
log.info("每5秒执行一次");
}
// 每分钟的第30秒执行
@Scheduled(cron = "30 * * * * *")
public void everyMinuteAt30() {
log.info("每分钟的第30秒执行");
}
// 每小时的第15分钟执行
@Scheduled(cron = "0 15 * * * *")
public void everyHourAt15() {
log.info("每小时的第15分钟执行");
}
// 每天10:30执行
@Scheduled(cron = "0 30 10 * * *")
public void dailyAt10_30() {
log.info("每天10:30执行");
}
/**
* 复杂的Cron表达式示例
*/
// 工作日的9点到17点,每半小时执行
@Scheduled(cron = "0 */30 9-17 * * MON-FRI")
public void workHoursTask() {
log.info("工作时间任务执行");
}
// 每月1号凌晨2点执行
@Scheduled(cron = "0 0 2 1 * ?")
public void monthlyTask() {
log.info("每月任务执行");
}
// 每个季度的第一天凌晨3点执行
@Scheduled(cron = "0 0 3 1 1,4,7,10 ?")
public void quarterlyTask() {
log.info("季度任务执行");
}
/**
* 特殊字符用法
*/
// 'L' 表示最后一天
@Scheduled(cron = "0 0 23 L * ?")
public void lastDayOfMonth() {
log.info("每月最后一天23:00执行");
}
// 'W' 表示最近的工作日
@Scheduled(cron = "0 0 12 15W * ?")
public void nearestWeekday() {
log.info("每月15号最近的工作日12:00执行");
}
// '#' 表示第几个星期几
@Scheduled(cron = "0 0 10 ? * 2#2") // 每月第二个星期一10:00
public void secondMondayOfMonth() {
log.info("每月第二个星期一执行");
}
/**
* 从配置文件读取Cron表达式
*/
@Scheduled(cron = "${task.cron.expression:0 0/30 * * * *}")
public void configurableCronTask() {
log.info("可配置的Cron任务执行");
}
/**
* 使用Zone指定时区
*/
@Scheduled(cron = "0 0 9 * * *", zone = "Asia/Shanghai")
public void timezoneSpecificTask() {
log.info("上海时区每天9:00执行");
}
/**
* 动态Cron表达式(使用SpEL)
*/
@Scheduled(cron = "#{@cronExpressionProvider.getCron()}")
public void dynamicCronTask() {
log.info("动态Cron表达式任务执行");
}
}
三、参数组合使用
3.1 参数优先级说明
cron参数优先级最高,如果设置了 cron,其他时间参数会被忽略fixedRate和fixedDelay不能同时使用initialDelay只能与fixedRate或fixedDelay一起使用
java
@Component
@Slf4j
public class ParameterCombinationExample {
/**
* 正确的组合:initialDelay + fixedRate
*/
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void validCombination1() {
log.info("initialDelay + fixedRate 组合任务");
}
/**
* 正确的组合:initialDelay + fixedDelay
*/
@Scheduled(initialDelay = 5000, fixedDelay = 3000)
public void validCombination2() {
log.info("initialDelay + fixedDelay 组合任务");
}
/**
* 错误的组合:fixedRate + fixedDelay (编译不会报错,但运行时只会生效一个)
*/
@Scheduled(fixedRate = 5000, fixedDelay = 3000)
public void invalidCombination() {
log.warn("fixedRate 和 fixedDelay 同时设置,这是不推荐的!");
// 实际上只会按照 fixedDelay 执行
}
/**
* Cron表达式与其他参数组合(cron优先级高,其他参数会被忽略)
*/
@Scheduled(cron = "0 */5 * * * *", fixedRate = 10000)
public void cronWithOtherParams() {
log.info("设置了cron和fixedRate,只会按照cron执行");
// fixedRate 参数会被忽略
}
}
四、动态配置参数
4.1 使用配置文件
yaml
# application.yml
scheduled:
tasks:
data-sync:
enabled: true
cron: "0 */30 * * * *"
initial-delay: 10000
report-generation:
enabled: false
cron: "0 0 2 * * *"
cache-refresh:
fixed-rate: 300000 # 5分钟
java
@Component
@Slf4j
@ConditionalOnProperty(name = "scheduled.tasks.data-sync.enabled", havingValue = "true")
public class ConfigurableScheduledTask {
/**
* 从YAML配置文件读取所有参数
*/
@Scheduled(
cron = "${scheduled.tasks.data-sync.cron}",
initialDelayString = "${scheduled.tasks.data-sync.initial-delay:0}"
)
public void dataSyncTask() {
log.info("数据同步任务执行");
}
/**
* 使用默认值的配置
*/
@Scheduled(fixedRateString = "${scheduled.tasks.cache-refresh.fixed-rate:600000}")
public void cacheRefreshTask() {
log.info("缓存刷新任务执行");
}
}
4.2 使用SpEL表达式
java
@Component
@Slf4j
public class SpELExample {
private final Environment environment;
public SpELExample(Environment environment) {
this.environment = environment;
}
/**
* 使用SpEL读取环境变量
*/
@Scheduled(fixedRateString = "#{${task.interval.base} * ${task.interval.multiplier:1}}")
public void spelCalculationTask() {
log.info("SpEL计算间隔的任务执行");
}
/**
* 根据条件动态设置间隔
*/
@Scheduled(fixedDelayString = "#{environment.getProperty('task.mode') == 'fast' ? 1000 : 5000}")
public void conditionalIntervalTask() {
log.info("条件间隔任务执行");
}
/**
* 调用Bean方法获取配置
*/
@Scheduled(cron = "#{@scheduleConfig.getDataExportCron()}")
public void dataExportTask() {
log.info("数据导出任务执行");
}
/**
* 复杂的SpEL表达式
*/
@Scheduled(
initialDelayString = "#{T(java.lang.Math).random() * 10000}",
fixedRateString = "#{5000 + T(java.lang.Math).random() * 5000}"
)
public void randomIntervalTask() {
log.info("随机间隔任务执行");
}
}
@Component
class ScheduleConfig {
@Value("${app.environment}")
private String environment;
public String getDataExportCron() {
if ("prod".equals(environment)) {
return "0 0 2 * * *"; // 生产环境:每天凌晨2点
} else {
return "0 */30 * * * *"; // 测试环境:每30分钟
}
}
}
五、其他知识与注意事项
5.1 多个@Scheduled注解
Spring 4.0 开始支持同一个方法上使用多个 @Scheduled 注解:
java
@Component
@Slf4j
public class MultipleSchedulesExample {
/**
* 同一个方法多个调度规则
* 方法会在多个时间点被调用
*/
@Scheduled(cron = "0 0 9 * * *") // 每天9点
@Scheduled(cron = "0 0 18 * * *") // 每天18点
@Scheduled(cron = "0 0/30 9-18 * * MON-FRI") // 工作日每30分钟
public void multiScheduledTask() {
log.info("多调度任务执行,当前时间: {}", LocalDateTime.now());
}
/**
* 使用 @Schedules 注解(与上面等价)
*/
@Schedules({
@Scheduled(fixedRate = 5000),
@Scheduled(fixedDelay = 10000),
@Scheduled(cron = "0 0 12 * * *")
})
public void usingSchedulesAnnotation() {
log.info("使用 @Schedules 注解的任务");
}
}
5.2 与 @Transactional 结合
事务注解要放在 @Scheduled 下面
java
@Component
@Slf4j
public class TransactionalScheduledTask {
@Autowired
private UserRepository userRepository;
/**
* 带事务的定时任务
* 注意:事务注解要放在 @Scheduled 下面
*/
@Scheduled(cron = "0 0 4 * * *") // 每天凌晨4点
@Transactional
public void cleanupInactiveUsers() {
log.info("开始清理非活跃用户");
try {
// 删除30天未登录的用户
LocalDate cutoffDate = LocalDate.now().minusDays(30);
int deletedCount = userRepository.deleteInactiveUsers(cutoffDate);
log.info("清理完成,删除 {} 个非活跃用户", deletedCount);
} catch (Exception e) {
log.error("清理非活跃用户失败", e);
// 事务会自动回滚
throw e;
}
}
/**
* 使用 REQUIRES_NEW 事务传播级别
* 每次执行都是独立的事务
*/
@Scheduled(fixedRate = 60000) // 每分钟
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatistics() {
// 更新统计信息,即使失败也不影响其他事务
log.info("更新统计信息");
}
}
5.3 常见陷阱
java
@Component
@Slf4j
public class CommonPitfalls {
/**
* 陷阱1:长时间运行的任务阻塞其他任务
* 解决方案:使用异步执行或调整线程池
*/
@Scheduled(fixedRate = 1000)
public void longRunningTask() {
try {
Thread.sleep(5000); // 任务执行5秒
log.info("长时间任务完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 陷阱2:任务抛出异常导致后续任务不再执行
* 解决方案:捕获异常并处理
*/
@Scheduled(fixedDelay = 2000)
public void exceptionProneTask() {
try {
// 可能抛出异常的代码
if (Math.random() > 0.8) {
throw new RuntimeException("随机异常");
}
log.info("任务正常执行");
} catch (Exception e) {
log.error("任务执行异常,但已处理", e);
// 可以选择重试、记录日志、发送告警等
}
}
/**
* 陷阱3:不正确的Cron表达式
*/
@Scheduled(cron = "0 0 * * * *") // 正确:每小时执行
public void correctCron() {
log.info("正确的Cron任务");
}
// @Scheduled(cron = "0 0 * * *") // 错误:缺少秒字段
// public void incorrectCron() {
// log.info("这个不会执行");
// }
/**
* 陷阱4:Bean未实例化就尝试调度
* 确保@Component或@Service注解被扫描到
*/
/**
* 陷阱5:忘记启用调度
* 必须在配置类上添加 @EnableScheduling
*/
}