在企业级应用开发中,定时任务是最基础也最常用的功能模块之一。我经历过多个需要定时执行任务的业务场景,比如:
这些场景的共同特点是需要按照固定时间规律执行特定业务逻辑。传统做法是使用Linux的Crontab,但在分布式环境下会遇到单点故障、任务重复执行等问题。Spring Boot提供的定时任务方案完美解决了这些痛点。
首先在pom.xml中添加Spring Boot Starter依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
然后在启动类上添加@EnableScheduling注解:
java复制@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
创建一个组件类,使用@Scheduled注解标记定时方法:
java复制@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
log.info("当前时间:{}", new Date());
}
}
这里使用了fixedRate参数,表示每5秒执行一次。实际开发中我们更常用的参数包括:
Cron表达式是定时任务的核心配置,由6-7个字段组成:
code复制秒 分 时 日 月 周 [年]
常用示例:
注意:Spring中的Cron表达式与Linux Crontab有细微差别,特别是周字段的表示方式不同
默认情况下,所有定时任务都在同一个线程池中执行。我们可以通过配置TaskScheduler实现多线程执行:
java复制@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
taskScheduler.setThreadNamePrefix("scheduled-task-");
taskScheduler.setErrorHandler(t -> log.error("定时任务执行异常", t));
taskRegistrar.setTaskScheduler(taskScheduler);
}
}
在集群部署时,需要防止任务被多个实例重复执行。常用解决方案包括:
这里展示基于Redis的分布式锁实现:
java复制@Scheduled(cron = "0 0 2 * * ?")
public void dailyReportTask() {
String lockKey = "task:report:" + LocalDate.now();
try {
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.MINUTES);
if(locked != null && locked) {
// 执行业务逻辑
generateDailyReport();
}
} finally {
redisTemplate.delete(lockKey);
}
}
建议为所有定时任务添加以下监控指标:
示例监控代码:
java复制@Scheduled(fixedDelay = 60000)
public void monitorTask() {
long start = System.currentTimeMillis();
try {
// 业务逻辑
checkSystemHealth();
// 记录成功指标
metrics.recordSuccess("healthCheck", System.currentTimeMillis() - start);
} catch (Exception e) {
// 记录失败指标
metrics.recordFailure("healthCheck");
// 发送报警
alertService.sendAlert("健康检查任务异常", e);
throw e;
}
}
java复制@Async
@Scheduled(fixedRate = 3600000)
public void asyncTask() {
// 耗时操作
}
java复制@Scheduled(fixedRate = 5000)
public void nonOverlappingTask() {
if(!lockService.tryLock("taskLock", 5, TimeUnit.SECONDS)) {
return;
}
try {
// 业务逻辑
} finally {
lockService.unlock("taskLock");
}
}
有时我们需要运行时修改任务执行时间,可以通过ScheduledTaskRegistrar实现:
java复制@Service
public class DynamicScheduler {
@Autowired
private ScheduledTaskRegistrar taskRegistrar;
private ScheduledFuture<?> future;
public void startTask(Runnable task, String cron) {
stopTask(); // 停止现有任务
future = taskRegistrar.getScheduler()
.schedule(task, new CronTrigger(cron));
}
public void stopTask() {
if(future != null) {
future.cancel(true);
}
}
}
对于需要可靠执行的任务,可以结合消息队列实现:
java复制@Scheduled(fixedDelay = 10000)
public void retryFailedMessages() {
List<Message> failedMessages = messageRepository.findFailedMessages();
failedMessages.forEach(msg -> {
try {
messageQueue.resend(msg);
msg.markAsProcessed();
} catch (Exception e) {
log.error("消息重发失败", e);
msg.recordRetry();
}
});
}
在实际项目中,我通常会建立一个定时任务管理中心,统一管理所有任务的配置、执行日志和监控指标。这样可以方便地掌握系统所有定时任务的运行状态,快速定位问题。对于关键业务任务,建议实现任务补偿机制,确保即使某次执行失败也不会影响业务连续性。