凌晨时段通常是系统负载最低的时候,这个时间点执行后台任务对用户体验影响最小。想象一下,如果我们在白天业务高峰期执行数据同步或者日志清理,很可能会导致数据库锁表或者磁盘IO飙升,直接影响线上服务的响应速度。而凌晨2-4点这个时间段,大部分用户都在休息,系统资源相对空闲,正是执行这些耗时操作的黄金窗口。
我在电商项目中就遇到过这样的场景:每天需要统计前一天的销售数据生成报表。最初我们选择在上午9点执行这个任务,结果经常收到用户投诉说系统卡顿。后来把任务调整到凌晨3点执行,问题迎刃而解。这就是合理利用定时任务调度带来的直接价值。
Cron表达式看起来像一串神秘的代码,其实它的结构非常有规律。标准的Cron表达式由6个(有时是7个)字段组成,分别表示:
code复制秒 分 时 日 月 周 [年]
举个例子,"0 0 2 * * ?"这个表达式可以拆解为:
注意这个问号?,它是个特殊字符,表示"不指定值"。在Quartz中,日和星期字段是互斥的,必须有一个设置为?。
这里整理几个实际项目中常用的Cron表达式:
特别提醒,不同框架对Cron表达式的支持可能有细微差别。比如Spring的@Scheduled注解就只支持6位表达式(不包括年),而Quartz支持7位。
首先在Maven项目中引入Quartz依赖:
xml复制<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
如果是Spring Boot项目,可以直接使用starter:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
下面展示一个完整的每日数据清理任务实现:
java复制public class DataCleanupJob implements Job {
private static final Logger logger = LoggerFactory.getLogger(DataCleanupJob.class);
@Override
public void execute(JobExecutionContext context) {
logger.info("开始执行每日数据清理任务");
try {
// 清理30天前的日志
cleanOldLogs(30);
// 备份重要数据
backupCriticalData();
logger.info("数据清理任务执行完成");
} catch (Exception e) {
logger.error("数据清理任务执行失败", e);
}
}
private void cleanOldLogs(int days) {
// 具体清理逻辑
}
private void backupCriticalData() {
// 数据备份逻辑
}
}
调度器配置类:
java复制public class DailyScheduler {
public static void main(String[] args) throws SchedulerException {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
JobDetail job = JobBuilder.newJob(DataCleanupJob.class)
.withIdentity("dataCleanupJob", "maintenanceGroup")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("dailyTrigger", "maintenanceGroup")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 3 * * ?")) // 每天3点执行
.build();
scheduler.scheduleJob(job, trigger);
scheduler.start();
}
}
如果项目已经使用Spring框架,且定时任务需求不复杂,可以直接使用Spring的@Scheduled注解:
java复制@Component
public class DailyReportGenerator {
@Scheduled(cron = "0 0 4 * * ?") // 每天4点执行
public void generateDailyReport() {
// 报表生成逻辑
}
@Scheduled(cron = "0 30 3 * * ?") // 每天3:30执行
public void syncExternalData() {
// 外部数据同步逻辑
}
}
记得在配置类上添加@EnableScheduling注解:
java复制@Configuration
@EnableScheduling
public class SchedulingConfig {
// 其他配置
}
有时候我们需要在不重启应用的情况下修改执行时间,这时可以使用环境变量:
java复制@Scheduled(cron = "${report.generate.cron}")
public void generateReport() {
// ...
}
然后在application.properties中配置:
code复制report.generate.cron=0 0 5 * * ?
定时任务最怕的就是静默失败。我曾经遇到过数据同步任务因为网络问题失败,但由于没有完善的异常处理,这个问题三天都没被发现。建议采用以下策略:
改进后的任务类示例:
java复制public class SafeDataSyncJob implements Job {
private static final int MAX_RETRY = 3;
@Override
public void execute(JobExecutionContext context) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
try {
doSync();
return;
} catch (Exception e) {
retryCount++;
if (retryCount == MAX_RETRY) {
alertAdmin(e);
}
}
}
}
private void doSync() {
// 实际同步逻辑
}
private void alertAdmin(Exception e) {
// 发送告警
}
}
在分布式环境中,定时任务需要特别注意避免重复执行。Quartz提供了多种解决方案:
Spring Boot下配置Quartz集群非常简单,只需要在application.properties中添加:
properties复制spring.quartz.job-store-type=jdbc
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000
建议为重要定时任务添加执行时长统计:
java复制@Around("@annotation(scheduled)")
public Object monitorTaskExecution(ProceedingJoinPoint pjp, Scheduled scheduled) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
metrics.recordExecutionTime(pjp.getSignature().getName(), duration);
}
}
例如,大数据量处理可以改造为:
java复制@Scheduled(cron = "0 0 2 * * ?")
public void processLargeData() {
List<Long> allIds = fetchAllIds();
allIds.parallelStream()
.forEach(this::processSingleItem);
}