1. Quartz定时任务新增功能实战解析
最近在重构公司后台任务系统时,我遇到了一个典型场景:需要在现有Quartz定时任务基础上新增支持动态Cron表达式配置和事务回滚功能。这个需求看似简单,实际落地时却踩了不少坑。今天就把整个实现过程和经验教训完整记录下来,给有类似需求的同行参考。
2. 核心需求与技术选型
2.1 业务场景分析
我们原有的定时任务系统存在两个痛点:
- Cron表达式硬编码在代码中,每次修改都需要重新部署
- 任务执行失败后没有完善的事务回滚机制,导致数据不一致
新方案需要实现:
- 动态加载数据库存储的Cron表达式
- 支持任务执行失败时自动回滚已操作的数据
- 与Spring Boot现有体系无缝集成
2.2 技术组件选型
| 技术点 | 选型方案 | 理由说明 |
|---|---|---|
| 调度框架 | Quartz 2.3.2 | 企业级调度标准,Spring原生支持 |
| 事务管理 | Spring声明式事务 | 与现有架构一致,配置简洁 |
| 持久化存储 | MySQL 8.0 | 公司标准数据库 |
| 集成方式 | Spring Boot Starter | 简化配置,自动装配 |
提示:Quartz版本选择2.3.x而非最新版,因为这是目前Spring Boot 2.7.x官方测试兼容的稳定版本
3. 动态Cron配置实现
3.1 数据库设计
创建任务配置表:
sql复制CREATE TABLE `sys_job` (
`job_id` bigint NOT NULL AUTO_INCREMENT,
`job_name` varchar(64) NOT NULL,
`job_group` varchar(64) NOT NULL,
`cron_expression` varchar(120) NOT NULL,
`status` char(1) DEFAULT '0',
PRIMARY KEY (`job_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 动态调度核心代码
java复制public class DynamicScheduler {
@Autowired
private Scheduler scheduler;
public void refreshSchedule() throws SchedulerException {
List<SysJob> jobs = jobMapper.selectAll();
for (SysJob job : jobs) {
TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 表达式有变化时更新触发器
if (!trigger.getCronExpression().equals(job.getCronExpression())) {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder
.cronSchedule(job.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
trigger = trigger.getTriggerBuilder()
.withIdentity(triggerKey)
.withSchedule(scheduleBuilder)
.build();
scheduler.rescheduleJob(triggerKey, trigger);
}
}
}
}
3.3 Cron表达式校验
实现自定义校验注解:
java复制@Constraint(validatedBy = CronValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CronValid {
String message() default "非法的Cron表达式格式";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class CronValidator implements ConstraintValidator<CronValid, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return CronExpression.isValidExpression(value);
}
}
4. 事务回滚机制实现
4.1 事务配置要点
yaml复制spring:
datasource:
platform: mysql
hikari:
auto-commit: false
transaction:
default-timeout: 30
rollback-on-commit-failure: true
4.2 任务类事务管理
java复制@Transactional(rollbackFor = Exception.class)
public class DataSyncJob implements Job {
@Override
public void execute(JobExecutionContext context) {
try {
// 业务操作1
serviceA.process();
// 业务操作2
serviceB.process();
// 手动提交
TransactionAspectSupport.currentTransactionStatus().flush();
} catch (Exception e) {
// 触发回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new JobExecutionException(e);
}
}
}
4.3 事务隔离实践
遇到的一个典型问题:长事务导致锁等待超时。解决方案:
- 将大任务拆分为多个小事务
- 设置合理的事务隔离级别:
java复制@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 30)
5. 常见问题排查实录
5.1 触发器不生效问题
现象:修改Cron表达式后任务未按新时间执行
排查步骤:
- 检查数据库记录是否更新成功
- 确认refreshSchedule方法是否被调用
- 查看Quartz日志是否有调度异常
- 检查Misfire策略配置
解决方案:
java复制// 正确配置Misfire策略
.withMisfireHandlingInstructionDoNothing()
5.2 事务不回滚问题
现象:任务抛出异常但数据未回滚
排查步骤:
- 确认方法是否为public
- 检查异常类型是否匹配rollbackFor
- 验证是否启用了事务管理器
- 查看数据库引擎是否为InnoDB
根本原因:自调用导致AOP代理失效
修复方案:
java复制// 通过ApplicationContext获取代理对象
((DataSyncJob)applicationContext.getBean("dataSyncJob")).execute();
6. 性能优化实践
6.1 线程池配置
properties复制# application.properties
spring.quartz.properties.org.quartz.threadPool.threadCount=10
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
6.2 集群部署方案
yaml复制spring:
quartz:
job-store-type: jdbc
properties:
org.quartz.jobStore.isClustered: true
org.quartz.jobStore.clusterCheckinInterval: 20000
org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
6.3 监控指标暴露
通过Actuator端点监控:
java复制@Bean
public QuartzMetricsBinder quartzMetrics(Scheduler scheduler) {
return new QuartzMetricsBinder(scheduler);
}
7. 完整配置示例
7.1 Spring Boot配置
java复制@Configuration
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactory(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setAutoStartup(true);
factory.setWaitForJobsToCompleteOnShutdown(true);
factory.setOverwriteExistingJobs(true);
return factory;
}
}
7.2 任务注册方式
java复制@Bean
public JobDetail dataSyncJobDetail() {
return JobBuilder.newJob(DataSyncJob.class)
.withIdentity("dataSyncJob")
.storeDurably()
.build();
}
@Bean
public Trigger dataSyncTrigger() {
return TriggerBuilder.newTrigger()
.forJob(dataSyncJobDetail())
.withIdentity("dataSyncTrigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/5 * * * ?"))
.build();
}
8. 经验总结
-
Cron动态更新:每次修改后必须调用rescheduleJob方法,单纯更新数据库不会生效
-
事务边界:单个Job中避免包含过多业务操作,建议拆分为多个小任务
-
异常处理:务必在Job内部捕获所有异常并转换为JobExecutionException
-
测试要点:
- 模拟网络中断测试事务回滚
- 修改系统时间测试Cron触发
- 高并发场景测试锁竞争
-
监控建议:
- 记录每次任务执行的开始/结束时间
- 监控长时间运行的任务
- 设置任务执行超时告警
这套方案已在生产环境稳定运行半年,日均处理任务量超过2万次。最大的收获是认识到定时任务看似简单,但要实现企业级可靠性需要处理好各种边界情况。特别是在分布式环境下,任务幂等性和数据一致性是需要持续关注的重点。
