SpringBoot+Quartz动态管理定时任务

前置理论:

1.小顶堆(适合任务少的,因为向下调整耗费性能)

堆:是一完全二叉树(除了最后一层节点其他层都达到最大节点数,且最后一层都靠左排列);堆中某个节点的值总不大于或不小于其父节点。

定时任务是基于小顶堆。每一个job都对应每一个节点,节点值里面存储job的到期时间(Delay),每次取堆顶的节点去执行。

插入元素:插入尾部,然后上浮(每次与n/(2^x)的位置济宁比较,时间复杂度O(log n)。

删除堆顶元素:将尾部元素放到堆顶,然后下浮。第一个任务(最近)执行后(堆顶元素)就进行这个操作。

2.时间轮算法(适合任务多)

*链表或者数组实现时间轮询:while-true-sleep。遍历数组,每个下标放置一个链表,链表节点放置任务,遍历到了就取出执行。

*round型时间轮询:任务上记录一个round,遍历到了就将round减一,为0时取出执行(需要遍历所有的任务,效率较低)。

*分层时间轮(cron表达式):使用多个时间维度的轮,天轮:记录几点执行,月轮:记录几号执行。月轮遍历到了,将任务取出放去天轮里面,即可实现几号几点执行。

一、JDK定时器:Timer使用及原理分析

1.timer的使用

2.timer的数据结构和原理分析

*TaskQueue:小顶堆,存放timeTask

*TimerThread:任务执行线程,死循环不断检查是否有任务需要开始执行,有就执行,还是在这个线程执行。

*单线程执行任务,会阻塞。Scheaule:任务执行超时,会导致后面的任务往后推移。预想在这个间隔执行的任务就没了;ScheduleAtFixedRate:任务超时可能导致下一个任务马上执行。

*运行时异常会导致timer线程终止

*任务调度是基于绝对时间的,对系统时间敏感。

3.timer中存在的问题

4.timer的应用场景分析

/**
 * Timer:
 * 放入queue(小堆)中去执行
 * 底层task调用run方法,单线程执行,任务时长超过间隔时间会乱
 * 单线程:任务阻塞,导致超时,解决:使用线程池
 */
public class TimerTest {
    public static void main(String[] args) {
        Timer timer = new Timer();  //任务启动
        for (int i = 0; i < 2 ;i++){
            TimerTask task = new FoolTimerTask("foo" + i);
            //添加任务
            /**
             * schedule:真正的执行时间,取决于上一个任务的结束时间 (任务超过间隔时间就会丢失任务)
             * scheduleAtFixedRate:严格按照预设时间 (任务超过间隔时间执行时间会乱)
             */
            timer.schedule(task,new Date(),2000);  //任务,启动时间,间隔时间
        }
    }
}
class FoolTimerTask extends TimerTask{

    private String name;

    public FoolTimerTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        long startTimer = System.currentTimeMillis();
        try {
            Thread.sleep(3000);
            //使用线程池避免任务阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTimer = System.currentTimeMillis();
        System.out.println(endTimer-startTimer);
    }
}

二、定时任务线程池解析

1.使用简介

2.单线程版和多线程版

单线程见上面代码

3.数据结构、原理分析

*ScheduledThreadPoolExecutor:

使用多线程执行任务,不会相互阻塞

如果线程失活,会新建线程执行任务(线程抛异常,任务会被丢弃,需要捕获异常)

DelayWorkQueue:小顶堆、无界队列

        1.在定时线程池中,最大线程数是没有意义的

        2.执行时间距离当前时间越近的任务就在队列的最前面

        3.用于添加ScheduleFutureTask(继承于FutureTask,实现RunnableScheduleFuture接口)

        4.线程池中的线程从DelayQueue中获取ScheduleFutureTask执行

        5.实现了Delay接口,可以通过getDelay方法来获取延迟时间

*SingleScheduledThreadPoolExecutor:

单线程的线程池:适用于单个后台的线程执行周期,同时又保证任务顺序执行。

ScheduleThreadPoolExecutor

//一定延迟之后只执行一次某个任务

public ScheduleFuture schedule(Runnable command, long delay, TimeUnit unit);

public ScheduleFuture schedule(Callable callable, long delay, TimeUnit unit);// 会有返回值

//一定延迟之后周期性的执行某个任务

//间隔是固定的,无论上一个任务是否执行完成

public ScheduleFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);

//时间是不固定的,其会在周期任务的上一个任务执行完成之后才开始计时,并在指定的时间间隔之后才开始执行任务

public ScheduleFuture scheduleAtSicedRate(Runnable command, long initialDelay, long delay, TimeUnit unit);

4.应用场景

适用于多个后台线程执行周期性任务,同时为了满足资源管理的需求限制后台的线程数

5.Leader-Follower模式

避免没必要的唤醒和阻塞的操作,所有的工作线程中只有一个leader线程,其他线程都是follower线程,都处理休眠中,当leader线程拿到任务后执行任务前,自己会变成follower线程,同时会选出新的leader来,才去执行任务。如果此时有下一个任务,就是新的leader线程来执行,并以此往复这个过程,当之前执行任务的线程执行完毕再回来时,会判断如果此时已经没有任务了,又或者有任务但是其他的线程作为leader线程,那么自己就休眠了,如果此时有任务但是没有leader线程,那么自己就会重新成为leader线程来执行任务。

/**
 * 基于线程池的Timer:
 * 解决单线程阻塞
 */
public class TimerPool {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        for (int i = 0; i < 2; i++) {
            scheduledThreadPool.schedule(new Task("task-" + i),2, TimeUnit.SECONDS);
            scheduledThreadPool.scheduleAtFixedRate(new Task("task-" + i),0,2, TimeUnit.SECONDS);
        }
    }
}

class Task implements Runnable {

    private String name;

    public Task(String name) {
        this.name = name;
    }

    public void run() {
        long startTimer = System.currentTimeMillis();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTimer = System.currentTimeMillis();
        System.out.println(endTimer - startTimer);
    }
}

三、定时任务框架-quartz

1.使用简介

2.各组件介绍

Job:

封装成JobDetail设置属性,

@DisallowConcurrentExecution:禁止并发地执行同一个job定义(jobDetail定义的)的多个实例

@PersistJobDataAfterExectuion:持久化jobDetail中的JobDataMap(对trigger中的datamap无效)

如果一个任务不是持久化的,泽当没有触发器关联它的时候,Quartz会从Scheduler中删除它

如果一个任务请求恢复,一般是该任务执行期间发生了系统崩溃或者其他关闭进程的操作,当服务再次启动的时候,会再次执行该任务,此时,JobExecutionContext.isRecovdering()会返回true

Trigger:触发器,指定执行时间,开始结束时间等

*优先级:同时触发的trigger之间会比较优先级,如果trigger是可恢复的,在恢复之后再调度,优先级不变

*misfire,错误触发:

1.判断misfire的条件:job到达触发时间时没有被执行,被执行的延迟时间超过了Quartz配置的misfireThreshold阈值

2.产生的原因:当job达到触发时间时,所有线程都被其他job占用,没有可用线程;在job需要触发的时间点,schedule停止了;job使用了@DisallowConcurrentExecution注解,job不能并发执行,当达到下一个job执行点的时候,上一个任务还没有执行完;job指定了过去的开始时间

3.策略:默认使用MISFIRE_INSTRUCTION_SMART_POLICY策略;

SimpleTrigger:now*相关的策略,会立即执行第一个misfire的任务,同时修改startTime和repeatCount,因此会重新计算finalFireTime,原计划执行时间会被打乱;next*相关策略,不会立即执行misfire的任务,也不会修改startTime和repeatCount,因此finalFireTimeu也没有改变,发生了misfire也还是按照原计划执行

CronTrigger:MISFIRE_INSTRUCTION_IGNORE_SMART_POLICY(Quartz不会判断发生了misfire,立即执行所有发生了misfire的任务,然后按原计划执行);MISFIRE_INSTRUCTION_FIRE_ONCE_NOW(立即执行第一个发生misfire的任务,忽略其他发生misfire的任务,然后按原计划执行);MISFIRE_INSTRUCTION_DO_NOTHING(所有发生misfire的任务都被忽略,只是按照原计划执行)

*clendar:用于排除时间段

*SimpleTrigger:具体时间点,指定间隔重复执行

*CronTrigger:cron表达式

Schedule:调度器,基于trigger的设定执行job

*SchedulerFactory:

创建Scheduler;

DirectScheduleFactory:在代码里定制Schedule参数;

StdScheduleFactory:读取classpath下的quartz.properties配置来实例化Schedule

*jobStore:

存储运行时inxi的,包括Trigger,Scheduler,jobDetail,业务锁等

RamJobStore(内存实现)

JobStoreTX(JDBC,事务由Quartz管理)

JobStoreCMT(JDBC,使用容器事务)

ClusteredJobStore(集群实现)

TerracottaJobStore(Terracotta中间件)

*ThreadPool:SimpleThreadPool;自定义线程池

JobDataMap:保存任务实例的状态信息

jobDetail:默认只在Job被添加到调度程序scheduler的时候,存储一次关于该任务的状态信息数据,可以使用注解@PersisJobDataAfterExecution注解标明在一个任务执行完毕之后就存储一次

trigger:任务被多个触发器引用的时候,根据不同的触发时机,可以提供不同的输入条件

3.组件关系架构分析

SpringBoot+Quartz动态管理定时任务_第1张图片

4.监听器及插件

5.java项目整合quartz

        
            org.springframework.boot
            spring-boot-starter-quartz
        
/**
 * @DisallowConcurrentExecution:禁止并发地执行同一个job定义(jobDetail定义的)的多个实例
 * @PersistJobDataAfterExectuion:持久化jobDetail中的JobDataMap(对trigger中的datamap无效)
 * Scheduler每次执行,都会根据JobDetail创建一个新的job实例,这样就可以规避并发访问的问题
 */
@DisallowConcurrentExecution
public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("MyJob executr:" + System.currentTimeMillis());
        //可以从缓存的Mao里面获取数据,同名会覆盖
        JobDataMap jobDetailMap = context.getJobDetail().getJobDataMap();
        JobDataMap triggerMap = context.getTrigger().getJobDataMap();
        JobDataMap mergeMap = context.getMergedJobDataMap();
    }
}
public class TestJob {

    public static void main(String[] args) {
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("job1","group1")  //设置job的名字和组
                .usingJobData("job","jobDetail")  //可以放入信息数据,在业务逻辑中获取
                .requestRecovery(true)  //job可恢复,在其执行的时候,scheduler发生硬关闭,则当scheduler重新启动的时候,该job会被重新执行,此时,该job的JobExecutionContext.isRecovering()返回true
                .build();
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "trigger1")
                .usingJobData("trigger","trigger")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever() //也可以用cron
                ).build();
        try {
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            scheduler.scheduleJob(jobDetail,trigger);
            scheduler.start();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

四、springboot+quartz实现集群部署

注意:重启需要清除数据库中quartz相关表中信息,不然misfire会导致执行出错

 pom


    org.springframework.boot
    spring-boot-starter-quartz

配置文件:spring-quartz.properties

//spring-quartz.properties

#===========================================================
#配置JobStore
#===========================================================

#JobDataMaps是否都为String类型,默认false
org.quartz.jobStore.useProperties=false
#表的前缀,默认QRTZ
org.quartz.jobStore.tablePrefix = QRTZ
#是都加入集群
org.quartz.jobStore.isClustere = true
#调度实例失效的检查时间间隔ms
org.quartz.jobStore.clusterCheckinInterval = 5000
#当设置为true时,此属性告诉Quartz在非托管JDBC连接上调用setTransactionIsolation
org.quartz.jobStore.txIsolationLevelReadCommitted = true
#数据保存方式为数据库持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#数据库代理类,一般org.quratz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库

#===========================================================
#Scheduler 调度器属性配置
#===========================================================
#调度标识名 集群中每一个实例都必须使用相同的名称
org.quartz.scheduler.instanceName = ClusterQuartz
#ID设置为自动获取 每一个必须不同
org.quartz.scheduler.instanceId = AUTO

#===========================================================
#配置ThreadPool
#===========================================================
#线程池的实现类(一般使用SimpleThreadPool即可满足所有客需求)
org.qquartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#指定线程数,一般设置为1-100之间的整数,根据系统资源配置
org.qquartz.threadPool.threadCount = 5
#设置线程的优先级
org.qquartz.threadPool.threadPriority = 5

调度器配置:

/**
 * 配置器调度类
 */
@Configuration
public class SchedulerConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean().getScheduler();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setSchedulerName("cluster_scheduler");  //设置调度器名字
        factory.setDataSource(dataSource);  //设置数据源
        factory.setApplicationContextSchedulerContextKey("application");  //在IOCbean中的名字
        factory.setQuartzProperties(quartzProperties());  //设置配置文件
        factory.setTaskExecutor(schedulerThreadPool());  //设置线程池
        factory.setStartupDelay(0);  //设置执行delay
        return factory;
    }

    /**
     * 获取properties
     */
    @Bean
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/spring-quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

    /**
     * 获取线程池配置
     */
    @Bean
    public Executor schedulerThreadPool(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());  //取主机的核心线程数
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
        executor.setQueueCapacity(Runtime.getRuntime().availableProcessors());
        return executor;
    }
}

springboot启动监听器:

/**
 * springboot项目启动监听器
 * 注意重启需要清除数据库的定时任务表信息,不然misfire会出错触发
 */
@Component
public class StartApplicationListener implements ApplicationListener {

    @Autowired
    private Scheduler scheduler;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        //定义触发器,注意同意触发器只能存在一个实例
        TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "group1");
        try {
            Trigger trigger = scheduler.getTrigger(triggerKey);
            if (trigger == null) {
                trigger = TriggerBuilder.newTrigger()
                        .withIdentity(triggerKey)
                        .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                        .startNow()  //设置立即启动,可不用
                        .build();
                JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                        .withIdentity("job1", "group1")
                        .build();

                //开始调度
                scheduler.scheduleJob(jobDetail, trigger);
                scheduler.start();
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

业务代码:

/**
 * springboot 整合quartz
 */
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class QuartzJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try{
            //打印任务相关信息
            String schedulerInstanceId = context.getScheduler().getSchedulerInstanceId();
            String name = context.getJobDetail().getKey().getName();
            //调度任务相关业务代码
            //TODO:...
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

find by:BV1WL4y177up

你可能感兴趣的:(spring)