配置文件
位置在 module/job/config/ScheduleConfig
@Configuration
public class ScheduleConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
//quartz参数
Properties prop = new Properties();
prop.put("org.quartz.scheduler.instanceName", "RenrenScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
//线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "25");
prop.put("org.quartz.threadPool.threadPriority", "5");
//JobStore配置
prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
//集群配置
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
// misfire 时间 单位 毫秒
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
//PostgreSQL数据库,需要打开此注释
//prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate");
factory.setQuartzProperties(prop);
factory.setSchedulerName("RenrenScheduler");
//延时启动
factory.setStartupDelay(30);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
//可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
factory.setOverwriteExistingJobs(true);
// 设置自动启动,默认为true
factory.setAutoStartup(true);
return factory;
}
}
注意点:
2.5.x版本以后 org.quartz.jobStore.class 需要手动配置
org.quartz.threadPool.threadCount 线程池线程配置,默认是10,我这边调整到了 25,超过这个数字,会导致 misfire
数据库存储
除去 quartz 的默认的表意外,renren 与定时任务相关的表有两张 schedule_job
, schedule_job_log
,前面一张表用来持久化储存我们的定时任务配置,后面一张表用来存储定时任务执行日志
Schedule_job ,里面 bean_name 是我们后面继承job创建的 bean 的名称,param 可以传入定时任务执行参数,cron 表达式指定执行的时间
日志表比较简单就不单独说了,后面说下日志记录是如何实现的
项目启动时是如何初始化quartz的
/**
* 项目启动时,初始化定时器
*/
@PostConstruct
public void init(){
List<ScheduleJobEntity> scheduleJobList = this.list();
for(ScheduleJobEntity scheduleJob : scheduleJobList){
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJob.getJobId());
//如果不存在,则创建
if(cronTrigger == null) {
ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
}else {
ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
}
}
}
项目启动时,会执行这段代码,加载 schedule_job 中的数据到 quartz 的表当中,确保 quartz 中执行的情况与我们自定义表中执行情况一样 schedule_job,但其实这里我觉得可能有个 bug,就是如果 quartz 中存在 我们自定义表中不存在的执行计划和任务呢,虽然这种情况一般是不太可能的
如何创建一个定时任务
首先我们需要集成 ITask 接口创建一个类,并重写 run() 方法
/**
* 定时任务接口,所有定时任务都要实现该接口
*
* @author Mark sunlightcs@gmail.com
*/
public interface ITask {
/**
* 执行定时任务接口
*
* @param params 参数,多参数使用JSON数据
*/
void run(String params);
}
@Component("myTask")
public class MyTask implements ITask{
private Logger logger = LoggerFactory.getLogger(getClass());
@Resource
MyService myService;
@Override
public void run(String params){
logger.debug("myTask定时任务正在执行,执行参数是: " + param);
}
}
记得需要用 @Component(“myTask”) 把我们的实现类注册成一个 bean
调用 saveJob 方法创建
/**
* 保存定时任务
*/
@SysLog("保存定时任务")
@RequestMapping("/save")
@RequiresPermissions("sys:schedule:save")
public R save(@RequestBody ScheduleJobEntity scheduleJob){
ValidatorUtils.validateEntity(scheduleJob);
scheduleJobService.saveJob(scheduleJob);
return R.ok();
}
@Override
// 保证事务
@Transactional(rollbackFor = Exception.class)
public void saveJob(ScheduleJobEntity scheduleJob) {
scheduleJob.setCreateTime(new Date());
scheduleJob.setStatus(Constant.ScheduleStatus.NORMAL.getValue());
// 1. 写 schedule_job 表
this.save(scheduleJob);
// 2. 同时在 quartz 中创建
ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
}
/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
try {
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId())).build();
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
// 3. 注意下这里使用的 misfire 策略,超过触发时间,直接丢弃,misfire 这块关联前面配置文件配置
.withMisfireHandlingInstructionDoNothing();
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder).build();
//放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleJobEntity.JOB_PARAM_KEY, scheduleJob);
scheduler.scheduleJob(jobDetail, trigger);
//暂停任务
if(scheduleJob.getStatus() == Constant.ScheduleStatus.PAUSE.getValue()){
pauseJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException e) {
throw new RRException("创建定时任务失败", e);
}
}
重点关注下 service 实现,首先写 schedule 库,然后写 quartz
utils 使用的是 cron trigger,这种也是最灵活的方式,另外一种方式是 Simple
定时任务创建好默认是关闭的,调用 resume 开启即可
定时任务是如何记录日志的
/**
* 定时任务
*
* @author Mark sunlightcs@gmail.com
*/
public class ScheduleJob extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
// 1. context 中可以拿到 schedule 信息,每一次任务调度都会创建一个这样的 context
ScheduleJobEntity scheduleJob = (ScheduleJobEntity) context.getMergedJobDataMap()
.get(ScheduleJobEntity.JOB_PARAM_KEY);
//2. 在 Spring 容器中根据 beanName 获取 bean
ScheduleJobLogService scheduleJobLogService = (ScheduleJobLogService) SpringContextUtils.getBean("scheduleJobLogService");
//数据库保存执行记录
ScheduleJobLogEntity log = new ScheduleJobLogEntity();
log.setJobId(scheduleJob.getJobId());
log.setBeanName(scheduleJob.getBeanName());
log.setParams(scheduleJob.getParams());
log.setCreateTime(new Date());
//任务开始时间
long startTime = System.currentTimeMillis();
try {
//执行任务
logger.debug("任务准备执行,任务ID:" + scheduleJob.getJobId());
// 3.这里根据 beanName 拿到 对象 然后调用反射执行
Object target = SpringContextUtils.getBean(scheduleJob.getBeanName());
Method method = target.getClass().getDeclaredMethod("run", String.class);
method.invoke(target, scheduleJob.getParams());
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
log.setTimes((int)times);
//任务状态 0:成功 1:失败
log.setStatus(0);
logger.debug("任务执行完毕,任务ID:" + scheduleJob.getJobId() + " 总共耗时:" + times + "毫秒");
} catch (Exception e) {
logger.error("任务执行失败,任务ID:" + scheduleJob.getJobId(), e);
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
log.setTimes((int)times);
//任务状态 0:成功 1:失败
log.setStatus(1);
log.setError(StringUtils.substring(e.toString(), 0, 2000));
}finally {
scheduleJobLogService.save(log);
}
}
}
我们继承扩展 QuartzJobBean
,重写 executeInternal
方法,实现扩展
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId())).build();
构造 JobDetail 的时候传入了我们扩展的 QuartzJobBean
使用反射调用我们需要执行的方法,并传入 我们需要传入的参数(很巧妙),然后类似环绕通知的方式,在方法执行的前后记录日志,就实现定时任务日志记录的扩展了
ps
参考博客
Quartz框架(一)—Quartz的基本配置 - 简书 (jianshu.com)
关于quartz 集群模式下如何使用行锁竞争来实现
关于 job 执行的状态机