使用quartz集群开发定时任务整理

       在微服务环境下,定时任务也需要独立为一个服务。这里使用spring+quartz搭建定时任务开发环境。

       在Config加载quartz.properties配置文件时,本地环境因为资源文件我们都存放在项目resource下,可使用ClassPathResource去拿到资源文件。可是在集成、测试、生产环境下,一般会把配置文件都拿出来统一放在项目外的一个文件中,而ClassPathResource会从项目根目录下开始查找资源,于是会拿不到项目外的quartz.properties,导致定时任务执行可能会与预期结果不一致,尤其是在集群环境中。读取资源文件可采用PathResouce读取配置文件的绝对路径。

       我们将调度信息存储在mysql中,按照quartz规范在数据库建立QRTZ_JOB_DETAILS,QRTZ_TRIGGERS等共11张表。建表sql可在quartz发型包中/docs/dbTables里看到。配置

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

org.quartz.jobStore.tablePrefix = QRTZ_ 

quartz.properties有几个配置可以注意下。集群的配置

org.quartz.jobStore.isClustered = true 开启集群特性

org.quartz.jobStore.clusterCheckinInterval = 20000 设置Scheduler实例节点检测频率,节点出现问题会被发现

org.quartz.jobStore.misfireThreshold = 60000 设置定时任务失火阈值,当前时间超过原定执行时间若是在阈值之内,就可以执行

       新建一个任务表用于存放我们配置要执行的任务信息quartz_config表,任务(组)名,触发器(组)名,执行类,cron表达式等,可以在前端页面对任务管理。在进行周期性任务状态变化检测时,需要取quartz_config内的值来进行判断。这里使用实现SchedulingConfigurer接口来完成动态定时任务

    /**

    * 执行定时任务.

    */

    @Override

    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        taskRegistrar.addTriggerTask(

                //1.添加任务内容(Runnable)

                () -> quartzManager.chickJobs(),

                //2.设置执行周期(Trigger)

                triggerContext -> {

                    //2.1 从数据库获取执行周期

                    String cron = env.getProperty("job.cron");

                    //2.2 合法性校验.

                    if (StringUtils.isEmpty(cron)) {

                        cron="0/5 * * * * ?";

                    }

                    //2.3 返回执行周期(Date)

                    return new CronTrigger(cron).nextExecutionTime(triggerContext);

                }

        );

    }

       如图,配置每分钟检测一次任务状态变化,检测功能在quartzManager.chickJobs()中实现。目前我们设置Job的状态有启动,暂停,删除,删除就逻辑删除,页面不展示,在quartz_config中以status字段来标示。我们点击启动任务后,做的操作就将status置为1,此时虽然显示已启动,可实际上是还未注册实例的,然后等待下一次任务状态检测,取到status为1的任务数据,然后使用scheduler.checkExists()检测当前scheduler实例是否已经存在该job,如果已经存在,则获取当前job实例的Cron表达式,判断任务的触发时间是否有变化,若有,则更新触发器

                // 触发器

                TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();

                // 触发器名,触发器组

                triggerBuilder.withIdentity(triggerName, triggerGroupName);

                triggerBuilder.startNow();

                // 触发器时间设定

                triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing());

                // 创建Trigger对象

                trigger = (CronTrigger) triggerBuilder.build();

                // 方式一 :修改一个任务的触发时间

                sched.rescheduleJob(triggerKey, trigger);

否则添加一个job实例

       动态检测时对暂停和删除的Job的处理逻辑是,先取出quartz_config中status不等于1的数据,然后判断scheduler中是否存在该Job,存在就证明该job还是已注册的状态,就将该job从调度器中移除。

          Scheduler sched = schedulerFactory.getScheduler();

            TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);

            log.info("===============remove Job:{}===============",jobName);

            sched.pauseTrigger(triggerKey);// 停止触发器

            sched.unscheduleJob(triggerKey);// 移除触发器

            sched.deleteJob(JobKey.jobKey(jobName, jobGroupName));// 删除任务

       在集群多节点时,动态检测管理job状态,还需要做进一步控制,之前就因为没做控制,在本地时,就一台服务器,一直运行无误,找原因找了许久。假设集群中的两台服务器同时执行了任务检测逻辑,此时有一个任务点击启动(status=1),正在等待检测逻辑开始运行添加进实例(即insert进JOB_DETAILS,TRIGGERS等表),两台服务器同时拿到了这条待添加的job,必定有一台服务器先将任务实例持久化到数据库,另一台服务器在执行sched.scheduleJob(jobDetail,trigger)时,执行到底层storeJob方法时,就会报出ObjectAlreadyExistsException异常

if (existingJob) {

                if (!replaceExisting) {

                    throw new ObjectAlreadyExistsException(newJob);

                }

                this.getDelegate().updateJobDetail(conn, newJob);

            } else {

                this.getDelegate().insertJobDetail(conn, newJob);

            }

       我们的处理方法是,在quartz_config表中新增一个process_status字段,来标示当前任务处理状态,1为待处理,2为处理中,3为处理完成,保证同时只有一个节点能执行该添加修改操作。quartz_config数据初始化改状态为1,暂停或修改Cron,都会讲process_status置为1,因为它是发生变化待检测逻辑处理的。这样的话,如上例,两节点同时执行下来,先到的一个会将process_status更新为2(处理中)。

      UPDATE plms_quartz_config SET process_status = #{processStatus,jdbcType=VARCHAR} WHERE job_name = #{jobName,jdbcType=VARCHAR} AND process_status = '1'

更新成功则返回result = 1,只有result = 1的时候才会执行接下来的添加修改操作。当前处理状态已经从1变成2了,因为where条件的限制,另一节点到此已经更新不到改状态了,所以返回result = 0,就不会再一次addJob或addTirgger,避免对象已存在异常。

       有些情况下,到了指定时间才触发某个任务的执行可能满足不了需求,我们需要能手动触发一个任务立即执行来完成有些特殊情况。立即触发一个任务,我判断了当前正在执行中的任务不能立即执行。在任务执行中,该任务真正触发时间到了,需要执行,会导致任务重复执行,job类上因加上@DisallowConcurrentExecution防止任务重复执行(集群都需要)。解决重复执行了,可是任务可能会因为misfire失火机制在空闲时间或者下个轮询周期补偿此次的错失执行。看业务需要,配置失火策略,此处防止只执行一次的任务多次执行,我选择了失火之后忽略该任务不做补偿执行,实现方法是在rescheduleJob或scheduleJob之前设置触发器时,如下:

riggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing());

此处手动触发立即执行任务

//执行任务

                JobDetail job = JobBuilder.newJob((Class<? extends Job>) Class.forName(performJobReqDTO.getJobClassName())).withIdentity(jobKey).storeDurably().build();

                scheduler.addJob(job, true);

                scheduler.triggerJob(jobKey);

                log.info("job:{}任务已手动触发",JSON.toJSONString(jobKey));

       又遇到了个坑。此处立即执行任务triggerJob触发后,会在JOB_DETAILS,TRIGGERS,SIMPLE_TRIGGERS表存入数据,触发完后会删除TRIGGER的数据,如若此时该任务正好处于刚点击了启动但是还未注册的情况,或者点立即执行马上又点击启动,因为立即执行导致JobDetail已经有该job的数据,任务状态检测的时候就不会将该任务新注册进去,导致只有jobDetail,但缺失触发器,该任务就永远不会执行。处理方法为,若任务处理状态为非处理完成,在立即执行触发后,清除该任务CronTrigger,SimpleTrigger,Trigger,JobDetail,当动态检测执行时,就能正常注册任务触发器。

       又有个坑,立即执行触发(scheduler.triggerJob(jobKey))后删除那几张表,可能会导致job实例不执行,任务触发成功,但是实际任务没跑,怀疑是triggerJob(jobKey)方法内部执行,开启一个线程后还需要去查表拿数据,清楚太快导致没拿到数据,就没跑成功,具体原因没仔细研究,我在此处的解决方法是删除之前让线程睡一秒,确保任务能正常执行。

       本次也是初次对集群轮询环境做这些大致的构建,做一下遇到的问题记录笔记。当然以上处理方式还有很多更好更严谨的处理方式有待优化。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容