任务调度 Quartz

任务调度

任务调度需求

1、定义触发规则,基于时刻、时间间隔、表达式
2、定义任务
3、方便配置;配置文件/配置中心
4、任务串行;A-B-C
5、任务并发,互不干扰
6、调度器,启动、中断、停止任务
7、集成Spring

任务调度工具对比

层次举例特点
操作系统Linux crontab Windows 计划任务只能执行简单脚本或者命令
数据库MySQL、Oracle可以操作数据。不能执行Java 代码
工具Kettle可以操作数据,执行脚本。没有集中配置
开发语言JDK Timer、ScheduledThreadPoolTimer:单线程 JDK1.5 之后:ScheduledThreadPool(Cache、Fiexed、Single):没有集中配置,日程管理不够灵活
容器Spring Task、@Scheduled不支持集群
分布式框架XXL-JOB,Elastic-Job

Quartz

api

依赖

<dependency>
	<groupId>org.quartz-scheduler</groupId>
	<artifactId>quartz</artifactId>
	<version>2.3.0</version>
</dependency>

默认配置文件

quartz.properties
包自带的,如果我们没有创建该配置文件,就用系统自带的

创建Job

public class MyJob implements Job
{
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException
    {
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        System.out.println(format.format(date));
    }
}

Job 包装成JobDetail

JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1", "group1")
.usingJobData("name","2673")
.usingJobData("moon",5.21F)
.build();

创建触发器Trigger

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(2)
                        .repeatForever())

创建调度器 Scheduler

调度器一定都是单例
SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();

JobDetail

是由Job包装而来

Trigger

JobDetail 和Trigger 比例是1比N
Trigger有4种子接口
子接口描述特点
SimpleTrigger简单触发器固定时刻或时间间隔,毫秒
CalendarIntervalTrigger基于日历的触发器比简单触发器更多时间单位,支持非固定时间的触发,例如一年可365/366,一个月可能28/29/30/31
DailyTimeIntervalTrigger基于日期的触发器每天的某个时间段
CronTrigger基于Cron 表达式的触发器

Calendar 的排除规则

名称用法
BaseCalendar为高级的Calendar 实现了基本的功能,实现了org.quartz.Calendar 接口
AnnualCalendar排除年中一天或多天
CronCalendar日历的这种实现排除了由给定的CronExpression 表达的时间集合。例如,您可以使用此日历使用表达式“* * 0-7,18-23?* *”每天排除所有营业时间(上午8 点至下午5 点)。如果CronTrigger 具有给定的cron 表达式并且与具有相同表达式的CronCalendar 相关联,则日历将排除触发器包含的所有时间,并且它们将彼此抵消。
DailyCalendar您可以使用此日历来排除营业时间(上午8 点- 5 点)每天。每个DailyCalendar 仅允许指定单个时间范围,并且该时间范围可能不会跨越每日边界(即,您不能指定从上午8 点至凌晨5 点的时间范围)。如果属性invertTimeRange 为false(默认),则时间范围定义触发器不允许触发的时间范围。如果invertTimeRange 为true,则时间范围被反转- 也就是排除在定义的时间范围之外的所有时间。
HolidayCalendar特别的用于从Trigger 中排除节假日
MonthlyCalendar排除月份中的指定数天,例如,可用于排除每月的最后一天
WeeklyCalendar排除星期中的任意周几,例如,可用于排除周末,默认周六和周日

Scheduler 调度器

由 StdSchedulerFactory 产生的,是单例
scheduler的方法主要有三类:
1、操作调度器本身,start、shutdown
2、操作trigger pauseTriggers()、resumeTrigger()
3、操作job ;scheduleJob()、unscheduleJob()、rescheduleJob()

Listener

针对job、调度器、触发器的三种监听

JobListener

方法作用
getName()返回JobListener 的名称
jobToBeExecuted()Scheduler 在JobDetail 将要被执行时调用这个方法
jobExecutionVetoed()Scheduler 在JobDetail 即将被执行,但又被TriggerListener 否决了时调用这个方法
jobWasExecuted()Scheduler 在JobDetail 被执行之后调用这个方法

TriggerListener

方法作用
getName()返回监听器的名称
triggerFired()Trigger 被触发,Job 上的execute() 方法将要被执行时,Scheduler 就调用这个方法
vetoJobExecution()在Trigger 触发后, Job 将要被执行时由Scheduler 调用这个方法。TriggerListener 给了一个选择去否决Job 的执行。假如这个方法返回true,这个Job 将不会为此次Trigger 触发而得到执行
triggerMisfired()Trigger 错过触发时调用
triggerComplete()Trigger 被触发并且完成了Job 的执行时,Scheduler 调用这个方法

SchedulerListener

方法太多

Spring 集成Quartz

把job、Trigger、scheduler定义成Bean

Job

<bean name="myJob1" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
	<property name="name" value="my_job_1"/>
	<property name="group" value="my_group"/>
	<property name="jobClass" value="com.gupaoedu.quartz.MyJob1"/>
	<property name="durability" value="true"/>
</bean>

Trigger

<bean name="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="name" value="my_trigger_1"/>
<property name="group" value="my_group"/>
<property name="jobDetail" ref="myJob1"/>
<property name="startDelay" value="1000"/>
<property name="repeatInterval" value="5000"/>
<property name="repeatCount" value="2"/>
</bean>
Scheduler
<bean name="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
	<list>
	<ref bean="simpleTrigger"/>
	<ref bean="cronTrigger"/>
	</list>
	</property>
</bean>

Test

public class QuartzTest {

    private static Scheduler scheduler;

    public static void main(String[] args) throws SchedulerException {
        // 获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("spring_quartz.xml");

        // 从容器中获取调度器
        scheduler = (StdScheduler) ac.getBean("scheduler");

        // 启动调度器
        scheduler.start();
    }

}

注解方式配置

@Configuration
public class QuartzConfig
{
    @Bean
    public JobDetail printTimeJobDetail()
    {
        return JobBuilder.newJob(MyJob3.class).withIdentity("gupaoJob").usingJobData("gupao", "职位更好的你").storeDurably().build();
    }

    @Bean
    public Trigger printTimeJobTrigger()
    {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
        return TriggerBuilder.newTrigger().forJob(printTimeJobDetail()).withIdentity("quartzTaskService").withSchedule(cronScheduleBuilder).build();
    }

    @Bean
    public SchedulerFactoryBean printTimeJobScheduler()
    {
        SchedulerFactoryBean bean = new SchedulerFactoryBean();
        bean.setStartupDelay(1);
        return bean;
    }
}

测试

ApplicationContext context = new AnnotationConfigApplicationContext(QuartzConfig.class);

        JobDetail jobDetail = (JobDetail) context.getBean(JobDetail.class);

        Trigger trigger = (Trigger) context.getBean(Trigger.class);

        scheduler = (Scheduler) context.getBean(Scheduler.class);

        scheduler.scheduleJob(jobDetail, trigger);

        scheduler.start();
SchedulerFactoryBean 实例化是内部会创建一个scheduler 对象。

Springboot集成quartz

任务配置管理

表JobDetail

在这里插入图片描述

数据操作与任务调度

public class SchedulerUtil {
	private static Logger logger = LoggerFactory.getLogger(SchedulerUtil.class);

	/**
	 * 新增定时任务
	 * @param jobClassName 类路径
	 * @param jobName 任务名称
	 * @param jobGroupName 组别
	 * @param cronExpression Cron表达式
	 * @param jobDataMap 需要传递的参数
	 * @throws Exception
	 */
	public static void addJob(String jobClassName,String jobName, String jobGroupName, String cronExpression,String jobDataMap) throws Exception {
		// 通过SchedulerFactory获取一个调度器实例
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler scheduler = sf.getScheduler();
		// 启动调度器
		scheduler.start();
		// 构建job信息
		JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass())
				.withIdentity(jobName, jobGroupName).build();
		// JobDataMap用于传递任务运行时的参数,比如定时发送邮件,可以用json形式存储收件人等等信息
		if (StringUtils.isNotEmpty(jobDataMap)) {
			JSONObject jb = JSONObject.parseObject(jobDataMap);
			Map<String, Object> dataMap =(Map<String, Object>) jb.get("data");
			for (Map.Entry<String, Object> m:dataMap.entrySet()) {
				jobDetail.getJobDataMap().put(m.getKey(),m.getValue());
			}
		}
		// 表达式调度构建器(即任务执行的时间)
		CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
		// 按新的cronExpression表达式构建一个新的trigger
		CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName)
				.withSchedule(scheduleBuilder).startNow().build();
		try {
			scheduler.scheduleJob(jobDetail, trigger);
		} catch (SchedulerException e) {
			logger.info("创建定时任务失败" + e);
			throw new Exception("创建定时任务失败");
		}
	}
	
	/**
	 * 停用一个定时任务
	 * @param jobName 任务名称
	 * @param jobGroupName 组别
	 * @throws Exception
	 */
	public static void jobPause(String jobName, String jobGroupName) throws Exception {
		// 通过SchedulerFactory获取一个调度器实例
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler scheduler = sf.getScheduler();
		scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName));
	}
	
	/**
	 * 启用一个定时任务
	 * @param jobName 任务名称
	 * @param jobGroupName 组别
	 * @throws Exception
	 */
	public static void jobresume(String jobName, String jobGroupName) throws Exception {
		// 通过SchedulerFactory获取一个调度器实例
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler scheduler = sf.getScheduler();
		scheduler.resumeJob(JobKey.jobKey(jobName, jobGroupName));
	}
	
	/**
	 * 删除一个定时任务
	 * @param jobName 任务名称
	 * @param jobGroupName 组别
	 * @throws Exception
	 */
	public static void jobdelete(String jobName, String jobGroupName) throws Exception {
		// 通过SchedulerFactory获取一个调度器实例
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler scheduler = sf.getScheduler();
		scheduler.pauseTrigger(TriggerKey.triggerKey(jobName, jobGroupName));
		scheduler.unscheduleJob(TriggerKey.triggerKey(jobName, jobGroupName));
		scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));
	}
	
	/**
	 * 更新定时任务表达式
	 * @param jobName 任务名称
	 * @param jobGroupName 组别
	 * @param cronExpression Cron表达式
	 * @throws Exception
	 */
	public static void jobReschedule(String jobName, String jobGroupName, String cronExpression) throws Exception {
		try {
			SchedulerFactory schedulerFactory = new StdSchedulerFactory();
			Scheduler scheduler = schedulerFactory.getScheduler();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
			// 表达式调度构建器
			CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
			CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
			// 按新的cronExpression表达式重新构建trigger
			trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).startNow().build();
			// 按新的trigger重新设置job执行
			scheduler.rescheduleJob(triggerKey, trigger);
		} catch (SchedulerException e) {
			System.out.println("更新定时任务失败" + e);
			throw new Exception("更新定时任务失败");
		}
	}
	
	/**
	 * 检查Job是否存在
	 * @throws Exception
	 */
	public static Boolean isResume(String jobName, String jobGroupName) throws Exception {
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler scheduler = sf.getScheduler();
		TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
		Boolean state = scheduler.checkExists(triggerKey);
	     
		return state;
	}

	/**
	 * 暂停所有任务
	 * @throws Exception
	 */
	public static void pauseAlljob() throws Exception {
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler scheduler = sf.getScheduler();
		scheduler.pauseAll();
	}

	/**
	 * 唤醒所有任务
	 * @throws Exception
	 */
	public static void resumeAlljob() throws Exception {
		SchedulerFactory sf = new StdSchedulerFactory();
		Scheduler sched = sf.getScheduler();
		sched.resumeAll();
	}

	/**
	 * 获取Job实例
	 * @param classname
	 * @return
	 * @throws Exception
	 */
	public static BaseJob getClass(String classname) throws Exception {
		try {
			Class<?> c = Class.forName(classname);
			return (BaseJob) c.newInstance();
		} catch (Exception e) {
			throw new Exception("类["+classname+"]不存在!");
		}
		
	}

}

前端界面

在这里插入图片描述
在这里插入图片描述

容器启动与Service 注入

任务定义在数据库,容器启动如何读取任务

利用CommandLineRunner接口,实现项目启动后读取job构建定时任务。
@Component
public class InitStartSchedule implements CommandLineRunner
{
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISysJobService sysJobService;
    @Autowired
    private MyJobFactory myJobFactory;

    @Override
    public void run(String... args) throws Exception
    {
        /**
         * 用于程序启动时加载定时任务,并执行已启动的定时任务(只会执行一次,在程序启动完执行)
         */

        //查询job状态为启用的
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("jobStatus", "1");
        List<SysJob> jobList = sysJobService.querySysJobList(map);
        if (null == jobList || jobList.size() == 0)
        {
            logger.info("系统启动,没有需要执行的任务... ...");
        }
        // 通过SchedulerFactory获取一个调度器实例
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();
        // 如果不设置JobFactory,Service注入到Job会报空指针
        scheduler.setJobFactory(myJobFactory);
        // 启动调度器
        scheduler.start();

        for (SysJob sysJob : jobList)
        {
            String jobClassName = sysJob.getJobName();
            String jobGroupName = sysJob.getJobGroup();
            //构建job信息
            JobDetail jobDetail = JobBuilder.newJob(getClass(sysJob.getJobClassPath()).getClass()).withIdentity(jobClassName, jobGroupName).build();
            if (StringUtils.isNotEmpty(sysJob.getJobDataMap()))
            {
                JSONObject jb = JSONObject.parseObject(sysJob.getJobDataMap());
                Map<String, Object> dataMap = (Map<String, Object>) jb.get("data");
                for (Map.Entry<String, Object> m : dataMap.entrySet())
                {
                    jobDetail.getJobDataMap().put(m.getKey(), m.getValue());
                }
            }
            //表达式调度构建器(即任务执行的时间)
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(sysJob.getJobCron());
            //按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName).withSchedule(scheduleBuilder).startNow().build();
            // 任务不存在的时候才添加
            if (!scheduler.checkExists(jobDetail.getKey()))
            {
                try
                {
                    scheduler.scheduleJob(jobDetail, trigger);
                } catch (SchedulerException e)
                {
                    logger.info("\n创建定时任务失败" + e);
                    throw new Exception("创建定时任务失败");
                }
            }
        }
    }

    public static BaseJob getClass(String classname) throws Exception
    {
        Class<?> c = Class.forName(classname);
        return (BaseJob) c.newInstance();
    }
}

Service 类注入到Job 中

Job实例是quartz管理的,Job中的service是spring管理的。Job中依赖service会空指针
利用 AutowireCapableBeanFactory 把 job实例注册到spring中
@Component
public class MyJobFactory extends AdaptableJobFactory
{
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception
    {
        //调用父类的方法
        Object jobInstance = super.createJobInstance(bundle);
        capableBeanFactory.autowireBean(jobInstance);

        return jobInstance;
    }
}

指定Scheduler 的JobFactory 为自定义的JobFactory
在这里插入图片描述
这里有个问题,貌似定时任务执行时都会创建一个Job实例。

Quartz 调度原理

获取调度器实例

入口从Scheduler scheduler = factory.getScheduler();进去

在这里插入图片描述

instantiate()负责初始化从工作,如果我们没有创建quartz.properties配置文件,
就加载默认的文件  org/quartz/quartz.properties
// 存储任务信息的JobStore
JobStore js = null;
// 创建线程池,默认是SimpleThreadPool
ThreadPool tp = null;
// 创建调度器
QuartzScheduler qs = null;
// 连接数据库的连接管理器
DBConnectionManager dbMgr = null;
// 自动生成ID
// 创建线程执行器,默认为DefaultThreadExecutor
ThreadExecutor threadExecutor;

创建线程池(包工头)
在这里插入图片描述
在这里插入图片描述

SimpleThreadPool里维护了三个List,分别存放所有工作线程、控线工作线程、忙碌工作线程

runInThread()是线程池运行线程的方法,参数runnable是要运行的内容
取出WorkerThread 去执行参数里面的runnable(JobRunShell)

在这里插入图片描述
WorkerThread(工人)

WorkerThread 是SimpleThreadPool 的内部类,用来执行任务。我们把
WorkerThread 理解为工人。在WorkerThread 的run 方法中,执行传入的参数runnable任务:

在这里插入图片描述
在这里插入图片描述

创建调度线程(项目经理)

QuartzScheduler qs = null;
qs = new QuartzScheduler(rsrcs, idleWaitTime, dbFailureRetry);
在QuartzScheduler 的构造函数中,创建了QuartzSchedulerThread,我们把它
理解为项目经理,它会调用包工头的工人资源,给他们安排任务。
并且创建了线程执行器schedThreadExecutor , 执行了这个
QuartzSchedulerThread,也就是调用了它的run 方法。

在这里插入图片描述

点开QuartzSchedulerThread 类,找到run 方法,这个是Quartz 任务调度的核心方法:

JobRunShell 的作用
JobRunShell 用来为Job 提供安全的运行环境的,执行Job 中所有的作业,捕获运行中的异常,在任务执行完毕的
时候更新Trigger 状态,等等。
JobRunShell 实例是用JobRunShellFactory 为QuartzSchedulerThread 创建的,在调度器决定一个Job 被触发的时候,
它从线程池中取出一个线程来执行任务。

绑定JobDetail 和Trigger

在这里插入图片描述

启动调度器

// 通知监听器
notifySchedulerListenersStarting();
if (initialStart == null) {
initialStart = new Date();
this.resources.getJobStore().schedulerStarted();
startPlugins();
} else {
resources.getJobStore().schedulerResumed();
}
// 通知QuartzSchedulerThread 不再等待,开始干活
schedThread.togglePause(false);
// 通知监听器
notifySchedulerListenersStarted();

源码总结

getScheduler 方法创建线程池ThreadPool,创建调度器QuartzScheduler,创建
调度线程QuartzSchedulerThread,调度线程初始处于暂停状态。
scheduleJob 将任务添加到JobStore 中。
scheduler.start()方法激活调度器,QuartzSchedulerThread 从timeTrriger 取出待
触发的任务, 并包装成TriggerFiredBundle , 然后由JobRunShellFactory 创建
TriggerFiredBundle 的执行线程JobRunShell , 调度执行通过线程池
SimpleThreadPool 去执行JobRunShell,而JobRunShell 执行的就是任务类的execute
方法:job.execute(JobExecutionContext context)。

集群原理

任务为什么重复执行

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值