为什么需要定时任务
定时任务(Scheduled Task)在开发中非常常用,它的核心作用就是 按照预定时间或间隔自动执行某些操作,无需人工干预。具体作用可以从以下几个方面理解
自动化执行重复任务
它可以避免人工手动触发操作,提高效率。
例如
-
每天凌晨清理过期数据
-
定时生成报表
-
定时发送通知或邮件
系统维护与监控
它可以定期检查系统状态、日志、数据库健康情况等。
例如
-
每隔5分钟监控服务器CPU、内存使用情况
-
每天备份数据库或文件
-
定期刷新缓存或重新加载配置
异步处理与任务调度
它结合异步线程池,可以 并发执行任务,提高系统吞吐量。
例如
-
高频数据采集(IoT设备、传感器)
-
批量消息处理或推送通知
实现延时或周期性逻辑
定时任务可以实现固定间隔、固定时间点、Cron表达式等灵活调度。
例如
-
每隔5秒执行一次任务 → fixedRate / fixedDelay
-
每天凌晨1点执行任务 → Cron表达式
下面,我们来讲一下如何在 spring 中继承定时任务
Java 配置
@Configuration
@EnableScheduling // 开启定时任务
public class TaskConfig {
@Scheduled(fixedRate = 1000) // 定义一个定时任务
public void scheduledTask() {
System.out.println("我永远喜欢雪之下雪乃");
}
}
@EnableScheduling
的作用是开启定时任务
@Scheduled
注解的作用是将被标注的方法注册为定时任务
来看一下该注解定义的一些参数
fixedRate
固定速率,每隔一段时间执行一次
它的开始时间是以上一次任务的开始执行的时间为基准的,隔 x 秒再触发下一次执行
如果上一次任务还没有完成,那么在此期间可以开始新任务吗
默认情况下,spring 的
@Scheduled
使用的是单线程调度器ThreadPoolTaskScheduler
,默认只有 1 个线程,所以即使在上一个线程还没执行完的期间,新线程也不会执行如果要实现同一个任务 并行执行,就要实现多线程任务调度器,需要自己配置
@Configuration @EnableScheduling public class TaskConfig { @Bean public ThreadPoolTaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); // 开 5 个线程 scheduler.setThreadNamePrefix("my-task-"); return scheduler; } @Scheduled(fixedRate = 1000) // 定义一个定时任务 public void scheduledTask() { System.out.println("我永远喜欢雪之下雪乃"); } }
这样,只要任务线程池中有还有空闲线程的情况下就,可以实现同一个任务并发执行
fixedDelay
固定延迟,任务执行完成后,延迟一定时间在执行
cron
配置 cron 表达式,可以精确控制时间
cron 表达式有 6~7 个字段
秒 分 时 日 月 周 [年]
字段含义
字段 | 取值范围 | 可以使用的特殊符号 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
时 | 0-23 | , - * / |
日 | 1-31 | , - * ? L W |
月 | 1-12 或 JAN-DEC | , - * / |
周 | 0-6(0=周日)或 SUN-SAT | , - * ? L # |
年 | 可选,1970-2099 | , - * / |
* → 任意值
, → 多个值
- → 范围
/ → 步长(如 0/5 表示每 5 单位一次)
? → 日和周字段里的“占位符”,避免冲突
L → 最后(如月的最后一天,周的最后一天=周六)
W → 工作日(如 15W 表示离 15 号最近的工作日)
# → 第几个星期几(如 2#1 表示每月第一个星期一)
常见表达式
表达式 | 含义 |
---|---|
0 0/5 * * * ? |
每 5 分钟执行一次 |
0 0 2 * * ? |
每天凌晨 2 点执行 |
0 0 9,18 * * ? |
每天上午 9 点、下午 6 点各执行一次 |
0 0/10 9-17 * * ? |
每天 9 点到 17 点之间,每隔 10 分钟执行一次 |
0 30 10 ? * MON-FRI |
周一到周五,每天 10:30 执行 |
0 0 0 L * ? |
每月最后一天的 0 点执行 |
0 0 0 1 1 ? |
每年 1 月 1 日 0 点执行 |
0 15 10 ? * 6L |
每月最后一个周五的 10:15 执行 |
实现 SchedulingConfigurer
接口可以进行更复杂的定制
@Configuration
@EnableScheduling
public class TaskConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
registrar.setScheduler(taskScheduler()); // 使用自定义线程池
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 并发线程数
scheduler.setThreadNamePrefix("my-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
return scheduler;
}
}
注册任务
registrar.addCronTask(() -> System.out.println("Task1"), "0/5 * * * * ?"); // 注册静态任务
registrar.addTriggerTask(
() -> System.out.println("Dynamic Task"),
triggerContext -> {
String cron = getCronFromDb(); // 比如从数据库/配置中心获取
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
); // 注册动态任务
registrar.addFixedRateTask(() -> System.out.println("FixedRate Task"), 5000); // 设置固定速率的定时任务
registrar.addFixedDelayTask(() -> System.out.println("FixedDelay Task"), 3000); // 设置固定间隔的定时任务
Spring 执行异步任务
上面我们了解到,如果只是使用框架默认提供的线程调度器,那么定时只能串行执行。这在一些场景下可能会影响效率。这时我们可以自定一个线程池,框架检测到 IoC 容器中存在其他线程池就不会使用单线程调度器,转而使用我们自己配置的多线程池。
除了这种方法,我们还可以使用异步线程
看配置
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
这是一段异步任务配置类,主要作用是定义一个自定义的线程池,用来执行 @Async
注解标注的异步方法
@EnableAsync
注解,启用 spring 的异步方法执行能力
@Async("taskExecutor")
public void asyncTask() {
// 这里的代码会异步执行,不会阻塞主线程
}
@Async
标注一个异步方法,它会使用自定的线程池异步执行
异步执行适合 IO密集型 或 后台批量任务
比如:异步写入 Redis、发送邮件、处理消息队列等
我们上面写的定时任务就可以使用该注解,使其变为一个可以并行的异步任务
@Scheduled(fixedRate = 1000) // 定义一个定时任务
@Async
public void scheduledTask() {
System.out.println("我永远喜欢雪之下雪乃");
}
当然,还有其他定时任务框架,如 Quartz,这里没用到,用到再说