Spring Boot - 定时任务
# Spring Boot - 定时任务
# 1. 定时任务的背景与业务价值
在企业级应用中,定时任务是一种不可或缺的功能组件,它允许系统在特定时间点自动执行预定义的操作,无需人工干预。定时任务在各类业务场景中有着广泛应用:
- 数据处理与同步:定期从外部系统导入/导出数据,同步多系统间的信息
- 系统维护操作:日志轮转、缓存清理、临时文件删除、数据库备份
- 业务提醒通知:定时发送会员到期提醒、账单通知、营销消息
- 报表统计生成:每日/周/月自动生成业务报表、数据分析结果
- 定期检查监控:监控系统运行状态、检测异常情况并预警
- 批量任务处理:将资源密集型任务安排在系统负载较低时执行
通过定时任务的合理应用,可以显著提升系统的自动化水平,减少人工干预,降低出错风险,同时优化资源利用效率,为用户提供更加及时、准确的服务体验。
# 2. Spring Boot定时任务框架的核心优势
相比传统的Quartz等复杂调度框架,Spring Boot内置的定时任务机制具有以下显著优势:
1. 简洁的声明式配置
Spring Boot采用注解驱动的方式实现定时任务,开发者只需添加@Scheduled
注解即可将普通方法转变为定时执行的任务,无需编写繁琐的配置代码。这种声明式的方式极大简化了定时任务的创建和管理流程。
2. 轻量级内置调度器
Spring Boot内置了一个高效的任务调度器,它提供了:
- 自动管理的线程池机制
- 任务执行状态跟踪
- 并发控制能力
- 异常处理机制
这一轻量级调度器无需额外依赖,即可满足大多数应用场景的需求。
3. 灵活多样的调度策略
@Scheduled
注解支持多种定时策略,包括:
- 基于Cron表达式:精确控制任务执行的时间点
- 固定频率执行:按设定的时间间隔重复执行
- 固定延迟执行:在上次执行完成后等待指定时间再次执行
- 初始延迟设置:首次执行前的等待时间
这些灵活的配置选项使开发者能够根据业务需求精确控制任务的执行时机和频率。
4. 无缝集成Spring生态
定时任务完全集成到Spring容器中,可以:
- 直接注入Spring管理的Bean
- 使用Spring的事务管理
- 结合Spring的AOP功能
- 利用Spring Boot的自动配置机制
# 3. 在Spring Boot中实现定时任务的步骤
# 3.1 添加必要依赖
首先,在项目的pom.xml
文件中添加相关依赖:
<!-- Spring Boot Web依赖,包含了Spring核心功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok简化代码(可选,但推荐使用) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
# 3.2 启用定时任务功能
在Spring Boot应用的启动类上添加@EnableScheduling
注解,激活定时任务的支持:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 应用程序启动类
* @EnableScheduling 注解用于开启定时任务调度功能
* 此注解必须添加,否则@Scheduled注解不会生效
*/
@EnableScheduling
@SpringBootApplication
public class SchedulingDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SchedulingDemoApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.3 创建定时任务类
创建一个包含定时任务方法的组件类:
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 定时任务示例类
* 使用@Component注解将其注册为Spring管理的Bean
* 使用@Slf4j注解自动创建日志对象
*/
@Component
@Slf4j
public class ScheduledTaskDemo {
/**
* 基于Cron表达式的定时任务
* cron = "0/5 * * * * ?" 表示每5秒执行一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void scheduledTaskWithCron() {
// 获取当前时间
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 记录日志
log.info("【Cron定时任务】在{}执行了任务", now);
}
/**
* 固定间隔的定时任务
* fixedRate = 10000 表示每10秒执行一次(从任务开始时计时)
* 无论上一次执行是否完成,都会按时启动下一次执行
*/
@Scheduled(fixedRate = 10000)
public void scheduledTaskWithFixedRate() {
log.info("【固定频率任务】开始执行 - {}", LocalDateTime.now());
try {
// 模拟任务执行耗时
Thread.sleep(2000);
} catch (InterruptedException e) {
log.error("任务执行被中断", e);
Thread.currentThread().interrupt();
}
log.info("【固定频率任务】执行完成 - {}", LocalDateTime.now());
}
/**
* 固定延迟的定时任务
* fixedDelay = 8000 表示上一次任务执行完成后,等待8秒再执行下一次
* 确保任务串行执行,不会重叠
*/
@Scheduled(fixedDelay = 8000)
public void scheduledTaskWithFixedDelay() {
log.info("【固定延迟任务】开始执行 - {}", LocalDateTime.now());
try {
// 模拟任务执行耗时
Thread.sleep(3000);
} catch (InterruptedException e) {
log.error("任务执行被中断", e);
Thread.currentThread().interrupt();
}
log.info("【固定延迟任务】执行完成 - {}", LocalDateTime.now());
}
/**
* 初始延迟的定时任务
* initialDelay = 5000 表示应用启动后延迟5秒再执行第一次任务
* fixedRate = 15000 表示之后每15秒执行一次
*/
@Scheduled(initialDelay = 5000, fixedRate = 15000)
public void scheduledTaskWithInitialDelay() {
log.info("【初始延迟任务】执行时间 - {}", LocalDateTime.now());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 4. Cron表达式:精确控制任务执行时间
# 4.1 Cron表达式格式说明
Cron表达式是一种强大的时间表达式,用于定义任务的精确执行时间。标准的Cron表达式由6或7个字段组成,用空格分隔:
[秒] [分] [时] [日] [月] [周] [年(可选)]
每个字段的取值范围及含义如下表所示:
字段名称 | 是否必须 | 允许的值范围 | 允许的特殊字符 | 表达含义 |
---|---|---|---|---|
秒(Second) | 是 | 0-59 | , - * / | 一分钟内的秒数 |
分(Minute) | 是 | 0-59 | , - * / | 一小时内的分钟数 |
时(Hour) | 是 | 0-23 | , - * / | 一天中的小时数 |
日(Day) | 是 | 1-31 | , - * ? / L W | 一个月中的第几天 |
月(Month) | 是 | 1-12或JAN-DEC | , - * / | 一年中的第几个月 |
周(Week) | 是 | 1-7或SUN-SAT | , - * ? / L # | 一周中的第几天(1=周日) |
年(Year) | 否 | 1970-2099 | , - * / | 指定年份(通常可省略) |
# 4.2 特殊字符含义详解
*
(星号):表示匹配该字段的所有值。例如,在分钟字段中使用*
意味着"每分钟"?
(问号):表示不指定值,仅用于日(Day)和周(Week)字段。由于这两个字段会相互影响,当你在其中一个字段指定了具体值时,另一个字段应使用?
占位-
(连字符):表示范围。例如,在小时字段中10-12
表示"10点、11点和12点",
(逗号):表示列举。例如,在星期字段中MON,WED,FRI
表示"周一、周三和周五"/
(斜杠):表示增量。例如,在秒字段中0/15
表示"从0秒开始,每15秒一次"(即0, 15, 30, 45秒)L
(L字母):表示"最后",具有不同含义,取决于使用的字段:- 在日字段中:表示月的最后一天
- 在周字段中:表示周六(也可以写成"7"或"SAT")
- 在日字段中的组合使用:如"L-3"表示月份的倒数第3天
W
(W字母):表示"工作日"(周一至周五),仅用于日字段。例如,15W
表示"离该月15日最近的工作日"#
(井号):表示"第几个星期几",仅用于周字段。例如,6#3
表示"第3个星期五"(6=星期五)
# 4.3 常用Cron表达式
Cron表达式 | 含义说明 |
---|---|
0 0 12 * * ? | 每天中午12点触发 |
0 15 10 ? * * | 每天上午10:15触发 |
0 0/30 9-17 * * ? | 工作时间内(9:00-17:00)每半小时触发 |
0 0 8 ? * MON-FRI | 每个工作日上午8:00触发 |
0 0 6 1 * ? | 每月1日凌晨6点触发 |
0 0 0 L * ? | 每月最后一天的零点触发 |
0 0 8 ? * 2#1 | 每月第一个星期一的上午8:00触发 |
0 30 23 L-3 * ? | 每月倒数第3天的23:30触发 |
0 0 0 ? * 6L | 每月最后一个星期五的零点触发 |
0 */5 * * * ? | 每5秒执行一次 |
0 0 */1 * * ? | 每小时执行一次 |
0 0 23 * * ? | 每天23:00执行一次 |
0 0 1 * * ? | 每天凌晨1:00执行一次 |
0 0 1 1 * ? | 每月1号凌晨1:00执行一次 |
# 4.4 在线Cron表达式生成与验证工具
对于不熟悉Cron表达式的开发者,可以使用在线工具生成和验证表达式,例如:
这些工具可以帮助您可视化Cron表达式的执行时间,确保表达式的正确性。
# 5. 定时任务的不同调度方式
Spring Boot的@Scheduled
注解支持多种调度方式,每种方式都有其特定的应用场景:
# 5.1 基于固定频率的调度(fixedRate)
/**
* 固定频率执行的任务示例
* fixedRate = 5000 表示每5秒执行一次,无论上一次执行是否完成
* timeUnit 属性用于指定时间单位,默认为毫秒(TimeUnit.MILLISECONDS)
*/
@Scheduled(fixedRate = 5000, timeUnit = TimeUnit.MILLISECONDS)
public void fixedRateTask() {
log.info("固定频率任务开始执行 - {}", LocalDateTime.now());
try {
// 模拟长时间运行的任务
log.info("固定频率任务执行中...");
Thread.sleep(3000); // 模拟任务耗时3秒
} catch (InterruptedException e) {
log.error("任务执行被中断", e);
Thread.currentThread().interrupt();
}
log.info("固定频率任务执行完成 - {}", LocalDateTime.now());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
关键特性:
- 任务按固定时间间隔执行,从上一次开始时间计算
- 如果任务执行时间超过设定的间隔,后续任务会立即执行(可能导致任务重叠)
- 适用场景:对执行频率有严格要求,允许并发执行的任务(如数据采集)
# 5.2 基于固定延迟的调度(fixedDelay)
/**
* 固定延迟执行的任务示例
* fixedDelay = 5000 表示上一次执行完成后等待5秒再次执行
* 确保任务串行执行,不会发生任务重叠
*/
@Scheduled(fixedDelay = 5000, timeUnit = TimeUnit.MILLISECONDS)
public void fixedDelayTask() {
log.info("固定延迟任务开始执行 - {}", LocalDateTime.now());
try {
// 模拟任务执行时间
log.info("固定延迟任务执行中...");
Thread.sleep(2000); // 模拟任务耗时2秒
} catch (InterruptedException e) {
log.error("任务执行被中断", e);
Thread.currentThread().interrupt();
}
log.info("固定延迟任务执行完成 - {}", LocalDateTime.now());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
关键特性:
- 任务在上一次执行完成后等待指定时间再次执行
- 确保任务串行执行,不会出现并发情况
- 实际执行间隔 = 任务执行时间 + 固定延迟时间
- 适用场景:任务需要串行处理,不允许并发执行的场景(如消息处理)
# 5.3 首次延迟执行(initialDelay)
/**
* 带初始延迟的定时任务示例
* initialDelay = 10000 表示应用启动后等待10秒再执行第一次任务
* fixedRate = 5000 表示之后每5秒执行一次
* 也可以与fixedDelay结合使用
*/
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void initialDelayTask() {
log.info("初始延迟任务执行 - {}", LocalDateTime.now());
// 任务执行逻辑...
}
2
3
4
5
6
7
8
9
10
11
12
关键特性:
- 首次执行前等待指定时间,避免应用启动时立即执行任务
- 可以与
fixedRate
或fixedDelay
结合使用 - 适用场景:系统启动后需要一段准备时间才能执行的任务
# 5.4 基于Cron表达式的精确调度
/**
* 基于Cron表达式的定时任务示例
* cron = "0 0/30 8-20 * * ?" 表示每天8:00-20:00之间,每30分钟执行一次
* zone 属性可以指定时区,默认使用系统默认时区
*/
@Scheduled(cron = "0 0/30 8-20 * * ?", zone = "Asia/Shanghai")
public void cronTask() {
log.info("Cron定时任务执行 - {}", LocalDateTime.now());
// 执行业务逻辑...
processDailyReport();
}
/**
* 模拟业务逻辑方法
*/
private void processDailyReport() {
log.info("正在生成业务报表...");
// 报表生成逻辑
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
关键特性:
- 基于Cron表达式提供最精确的时间控制
- 可以设置复杂的执行计划,如"每月第一个周一的8:15"
- 支持时区设置,确保在全球化应用中的准确执行
- 适用场景:需要在特定时间点执行的任务(如报表生成、数据备份)
# 6. 定时任务的高级配置
# 6.1 在application.properties中配置定时任务
Spring Boot允许在配置文件中动态设置定时任务参数:
# 定时任务线程池大小配置
spring.task.scheduling.pool.size=5
# 线程名称前缀
spring.task.scheduling.thread-name-prefix=scheduled-task-
# 在关闭应用时等待任务完成
spring.task.scheduling.shutdown.await-termination=true
# 关闭等待超时时间(秒)
spring.task.scheduling.shutdown.await-termination-period=60s
2
3
4
5
6
7
8
9
10
11
在代码中使用配置值:
@Scheduled(fixedRateString = "${task.report.rate:60000}")
public void configuredTask() {
log.info("使用配置参数的定时任务执行");
// 执行任务...
}
2
3
4
5
# 6.2 异步执行定时任务
默认情况下,Spring的定时任务是在单线程中顺序执行的。对于耗时任务,可以配置异步执行:
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Async;
/**
* 启用异步执行功能
*/
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class AsyncSchedulingApplication {
// ...
}
@Component
@Slf4j
public class AsyncScheduledTasks {
/**
* 异步执行的定时任务
* @Async注解使任务在单独的线程中执行
*/
@Async
@Scheduled(fixedRate = 5000)
public void asyncTask() {
log.info("异步定时任务开始执行 - 线程: {}", Thread.currentThread().getName());
try {
// 模拟耗时操作
Thread.sleep(3000);
} catch (InterruptedException e) {
log.error("任务被中断", e);
Thread.currentThread().interrupt();
}
log.info("异步定时任务执行完成 - 线程: {}", Thread.currentThread().getName());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 6.3 自定义任务调度器配置
对于有特殊需求的应用,可以自定义任务调度器:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 自定义任务调度器配置类
*/
@Configuration
public class SchedulingConfig {
/**
* 创建自定义的任务调度器Bean
* 可以根据需要调整线程池大小、线程优先级等参数
*/
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 设置线程池大小
scheduler.setPoolSize(10);
// 设置线程名前缀
scheduler.setThreadNamePrefix("custom-scheduler-");
// 设置等待任务完成再关闭
scheduler.setWaitForTasksToCompleteOnShutdown(true);
// 设置等待超时时间
scheduler.setAwaitTerminationSeconds(60);
// 设置线程优先级
scheduler.setThreadPriority(Thread.MAX_PRIORITY);
// 设置任务装饰器(可选)
// scheduler.setTaskDecorator(runnable -> {
// return () -> {
// try {
// MDC.put("scheduler", "true");
// runnable.run();
// } finally {
// MDC.remove("scheduler");
// }
// };
// });
return scheduler;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 6.4 处理任务异常
定时任务中的异常处理是确保系统稳定性的关键:
@Scheduled(fixedRate = 10000)
public void taskWithErrorHandling() {
try {
log.info("执行可能出错的定时任务");
// 模拟可能抛出异常的业务逻辑
if (System.currentTimeMillis() % 3 == 0) {
throw new RuntimeException("模拟任务执行失败");
}
log.info("任务执行成功");
} catch (Exception e) {
// 捕获并记录异常,防止任务中断
log.error("定时任务执行出错: {}", e.getMessage(), e);
// 可以在这里添加告警通知代码
sendAlertNotification(e);
}
}
/**
* 模拟发送告警通知
*/
private void sendAlertNotification(Exception e) {
log.warn("发送任务失败告警通知: {}", e.getMessage());
// 实际告警逻辑...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 6.5 条件性启用定时任务
有时需要根据环境或配置条件决定是否启用某些定时任务:
@Component
@Slf4j
public class ConditionalScheduledTasks {
private final Environment environment;
@Value("${task.enabled:false}")
private boolean taskEnabled;
public ConditionalScheduledTasks(Environment environment) {
this.environment = environment;
}
@Scheduled(fixedRate = 60000)
public void conditionalTask() {
// 仅在特定环境或配置下执行任务
String[] activeProfiles = environment.getActiveProfiles();
boolean isProductionProfile = Arrays.asList(activeProfiles).contains("production");
if (!taskEnabled || isProductionProfile) {
log.debug("条件不满足,跳过定时任务执行");
return;
}
log.info("条件满足,执行定时任务");
// 执行任务逻辑...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 7. 定时任务使用注意事项
# 7.1 避免长时间运行的任务
定时任务应设计为短小精悍,避免长时间执行:
@Scheduled(fixedRate = 60000)
public void processData() {
log.info("开始处理数据");
// 不要一次处理太多数据
// 错误示例: List<Data> allData = repository.findAll();
// 正确做法:分批处理
int page = 0;
int pageSize = 100;
Page<Data> dataPage;
do {
// 分页查询数据
dataPage = repository.findAll(PageRequest.of(page, pageSize));
// 处理当前页数据
processDataBatch(dataPage.getContent());
page++;
} while (dataPage.hasNext());
log.info("数据处理完成");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 7.2 确保任务的幂等性
定时任务应设计为幂等的,即多次执行不会产生意外结果:
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行
public void generateDailyReport() {
LocalDate today = LocalDate.now();
String reportDate = today.format(DateTimeFormatter.ISO_LOCAL_DATE);
log.info("开始生成{}的日报表", reportDate);
// 检查报表是否已存在,避免重复生成
if (reportRepository.existsByReportDateAndType(reportDate, "DAILY")) {
log.info("{}的日报表已存在,跳过生成", reportDate);
return;
}
// 生成并保存报表
Report report = reportService.generateDailyReport(today);
reportRepository.save(report);
log.info("{}的日报表生成完成", reportDate);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 7.3 使用分布式锁避免集群环境下的重复执行
在集群环境中,定时任务可能在多个节点上同时执行,应使用分布式锁确保只有一个实例执行任务:
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void dataCleanupTask() {
String lockKey = "scheduled:dataCleanup";
boolean acquired = false;
try {
// 尝试获取分布式锁(示例使用Redis实现)
acquired = redisLockService.tryLock(lockKey, 30, TimeUnit.MINUTES);
if (!acquired) {
log.info("无法获取分布式锁,跳过数据清理任务");
return;
}
log.info("获取分布式锁成功,开始执行数据清理任务");
// 执行实际的数据清理逻辑
cleanupExpiredData();
log.info("数据清理任务执行完成");
} finally {
// 确保释放锁
if (acquired) {
redisLockService.unlock(lockKey);
log.info("已释放分布式锁");
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 7.4 监控和记录定时任务执行情况
为了便于问题排查和性能优化,应记录任务执行情况:
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void syncExternalData() {
String taskId = UUID.randomUUID().toString();
log.info("开始同步外部数据,任务ID: {}", taskId);
long startTime = System.currentTimeMillis();
int processedCount = 0;
try {
// 记录任务开始
taskHistoryService.recordTaskStart("SYNC_EXTERNAL_DATA", taskId);
// 执行实际同步逻辑
processedCount = externalDataService.syncData();
// 记录任务成功完成
long duration = System.currentTimeMillis() - startTime;
taskHistoryService.recordTaskSuccess(taskId, duration,
Map.of("processedCount", String.valueOf(processedCount)));
log.info("外部数据同步完成,处理记录数: {},耗时: {}ms", processedCount, duration);
} catch (Exception e) {
// 记录任务失败
long duration = System.currentTimeMillis() - startTime;
taskHistoryService.recordTaskFailure(taskId, duration, e.getMessage());
log.error("外部数据同步失败,任务ID: {}, 错误: {}", taskId, e.getMessage(), e);
// 可选:触发告警
alertService.sendAlert("定时任务失败",
String.format("外部数据同步任务(ID:%s)失败: %s", taskId, e.getMessage()));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33