定时任务是企业应用中最常见的需求之一:半夜对账、每小时同步数据、每分钟清理过期会话、每天发送报表邮件。Spring 提供了两个层级的定时方案——轻量级的 @Scheduled 适合单体或简单定时需求,集成 Quartz 则支撑分布式的、动态的、带持久化的复杂调度场景。
14.5.1 @Scheduled 轻量定时
@Scheduled 是 Spring 对 JDK ScheduledExecutorService 的声明式封装,无需额外依赖,只需两步即可启用。
1. 启用定时任务支持
在任何一个配置类上标注 @EnableScheduling,告诉 Spring 容器要扫描带有 @Scheduled 的方法。
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
Spring Boot 项目会自动识别此注解,如果将它放在主启动类上也是常见做法。
2. 编写定时方法
将 @Scheduled 加在 Bean 的某个方法上,支持三种时间表达:
- fixedRate:以固定频率执行,从上一次开始时间算起。
- fixedDelay:以固定延迟执行,从上一次结束时间算起。
- cron:使用 Cron 表达式,精确控制到秒、分、时、日、月、周。
@Component
public class SimpleTasks {
// 每隔5秒执行一次(以开始时间算)
@Scheduled(fixedRate = 5000)
public void syncData() {
System.out.println("同步数据:" + Thread.currentThread().getName());
}
// 上次执行结束后等3秒再执行
@Scheduled(fixedDelay = 3000)
public void cleanExpiredTokens() {
// 清理逻辑
}
// 每天凌晨 2:30 执行
@Scheduled(cron = "0 30 2 * * ?")
public void dailyReport() {
// 生成日报
}
}
3. 配置线程池
默认情况下,@Scheduled 方法由只有一个线程的公共线程池执行。这意味着如果多个定时任务同时触发,它们会排队等待;如果一个任务长时间未完成,会阻塞其他所有任务。生产环境必须自定义线程池。
@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean
public Executor taskExecutor() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 核心线程数
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
}
4. 使用 @Scheduled 的局限与注意事项
- 单点执行:
@Scheduled基于本地内存调度,多实例部署时同一任务会在每个节点各自执行一次。若要避免重复执行,需要借助分布式锁(如 Redis setnx、ShedLock 等)。 - 动态管理困难:修改执行时间需要改代码重新发布,或借助
@Scheduled的fixedDelayString等从配置文件读取,但依然无法在运行时增删定时任务。 - 无持久化与监控:任务执行状态、失败记录全依赖日志,没有原生的失败重试和任务追踪界面。
轻量场景下,@Scheduled 足够简单高效。但遇到动态调度、分布式任务、任务持久化和可视化监控需求时,Quartz 是更专业的选择。
14.5.2 Quartz 分布式定时
Quartz 是 Java 领域中久经考验的调度框架,支持任务持久化到数据库、集群环境下的分布式调度(多个节点争抢执行同一任务,保证不会重复触发),以及丰富的 Cron 表达式和日历排除规则。
Spring 提供了与 Quartz 的深度集成,核心是使用 SchedulerFactoryBean 作为 Quartz 调度器的生命周期管理 Bean。
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
同时需要数据库驱动和 Quartz 专用表(官方提供了针对各数据库的建表脚本)。
2. 创建 Job
Quartz 的 Job 是实现 org.quartz.Job 接口的类。为了方便注入 Spring Bean,Spring 提供了 QuartzJobBean 基类,将 Job 也纳入容器管理。
@Component
public class OrderReconciliationJob extends QuartzJobBean {
@Autowired
private OrderService orderService;
@Override
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
// 真正的业务逻辑
orderService.reconcileOrders();
}
}
3. 配置 JobDetail 和 Trigger
JobDetail 描述了 Job 的身份和额外参数,Trigger 决定何时触发。Spring 提供 JobDetailFactoryBean 和 CronTriggerFactoryBean 来将它们注册为容器 Bean。
@Configuration
public class QuartzConfig {
@Bean
public JobDetail reconciliationJobDetail() {
return JobBuilder.newJob(OrderReconciliationJob.class)
.withIdentity("reconciliationJob")
.storeDurably() // 即使没有 Trigger 也持久化
.build();
}
@Bean
public Trigger reconciliationTrigger() {
return TriggerBuilder.newTrigger()
.forJob(reconciliationJobDetail())
.withIdentity("reconciliationTrigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) // 每天2点
.build();
}
}
当 Spring 容器启动时,SchedulerFactoryBean 会自动创建 Quartz Scheduler,将 Scheduler 暴露为 Bean,并加载定义好的 Trigger。
4. 配置 Quartz 集群与持久化
要让 Quartz 支持分布式和任务持久化,必须配置 quartz.properties 并指向数据库。
spring:
quartz:
job-store-type: jdbc # 使用数据库存储 Job 和 Trigger
jdbc:
initialize-schema: never # 表结构已经手动创建
properties:
org:
quartz:
scheduler:
instanceName: MyClusteredScheduler
instanceId: AUTO # 集群节点自动生成 ID
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 20000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
在集群模式下,多个应用节点共享同一套 Quartz 表。Quartz 通过数据库行锁保证同一个 Job 只会被一个节点成功索取并执行,若节点宕机,其他节点会发现并接管任务。
5. 动态管理任务
Quartz 的优势之一是支持运行时动态增删改查任务,通过注入 Scheduler 可以直接操作:
@Service
public class QuartzManageService {
@Autowired
private Scheduler scheduler;
// 暂停一个触发器
public void pauseJob(String triggerName, String triggerGroup)
throws SchedulerException {
scheduler.pauseTrigger(TriggerKey.triggerKey(triggerName, triggerGroup));
}
// 动态添加一次性任务
public void addOneTimeJob(String jobName, JobDataMap data)
throws SchedulerException {
JobDetail jobDetail = JobBuilder.newJob(SomeSimpleJob.class)
.withIdentity(jobName)
.usingJobData(data)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobName + "_trigger")
.startAt(DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND))
.build();
scheduler.scheduleJob(jobDetail, trigger);
}
}
结合控制台页面或 API,即可实现任务的可视化管理。
6. 监听与链式任务
Quartz 提供丰富的监听器接口,可以在任务执行前后、触发完成时植入业务逻辑,比如记录执行日志、发送报警通知。
@Component
public class JobExecutionLogListener extends JobListenerSupport {
@Override
public String getName() {
return "ExecutionTimeLogListener";
}
@Override
public void jobWasExecuted(JobExecutionContext context,
JobExecutionException jobException) {
long duration = context.getJobRunTime();
String jobKey = context.getJobDetail().getKey().toString();
log.info("Job {} 执行耗时 {} ms", jobKey, duration);
if (jobException != null) {
log.error("Job {} 执行异常", jobKey, jobException);
}
}
}
然后在 SchedulerFactoryBean 中注册该监听器即可全局生效。
14.5.3 选型建议
@Scheduled:适合开发测试、小型项目、定时任务数量少且无需动态管理的场景。结合@SchedulerLock(ShedLock)能简单实现多节点互斥执行。- Quartz:适合任务数量多、有动态调度需求、要求任务持久化、需要集群高可用的企业级场景。成熟稳定,但引入了数据库依赖和一定的复杂性。
在某些微服务架构中,也可以考虑将定时任务统一托管到专门的调度中心(如 XXL-JOB、Elastic-Job),进一步降低与业务代码的耦合。但就 Spring 生态内部而言,@Scheduled + Quartz 已经覆盖了绝大多数定时调度需求。