基于quartz的定时任务动态启停实现分析(人人平台为例)

配置文件

位置在 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,前面一张表用来持久化储存我们的定时任务配置,后面一张表用来存储定时任务执行日志

image-20221102151815572

Schedule_job ,里面 bean_name 是我们后面继承job创建的 bean 的名称,param 可以传入定时任务执行参数,cron 表达式指定执行的时间

image-20221102151759432

日志表比较简单就不单独说了,后面说下日志记录是如何实现的

项目启动时是如何初始化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 执行的状态机

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值