统一任务调度设计

一:系统涉及的要点

1.很多微服务项目都需要集成作业调度模块

在调用api创建调度任务,需要传入执行的业务逻辑类名,这个逻辑需要对接的业务系统提供放在对应的文件目录下,这样当触发条件执行调度任务才能找到对应的文件执行

2.作业调度系统在整体项目体系中属于底层基础组件项目,整体需易于接入公司的监控和规范

如任务执行出现问题,要有及时的报警以便及时跟进,定期日报汇总以便于掌握整体微服务系统作业的调度情况

3.作业调度系统要非常易于接入

利用微服务之间feign之间调用

/**
 * 调度任务接口
 */
@FeignClient(name = "cloud-job", path = "/job/api", url = "${service-api.job:}")
public interface JobServiceFeign {

    @PostMapping("/createJob")
    ApiResult createJob(JobDTO jobDTO) throws ClassNotFoundException;
}

创建任务:/job/api/createJob

入参:

{"triggerName":"sms-job", //服务名称

"triggerGroupName":"zinger", //归属系统名称

"cron":"0 0 0-3 * * ?", //执行周期,为空情况下,在创建任务后的十秒后触发执行调度

"jobData":"{'activity_id':'1111','b':'2'}", //入参

"moduleName":"SmsQuartzJob" //执行的逻辑module类名称

}

修改一个任务执行时间:/job/api/modifyJobTime

暂停一个任务:/job/api/pauseJob

恢复一个任务:/job/api/resumeJob

移除一个任务:/job/api/closeJob

查询一个任务状态:/job/api/getJobStatus

4.权限设计,调用api权限控制

新建一个feign拦截器模块,在feign拦截 模块写一个类实现RequestInterceptor实现apply方法 拦截请求

实现逻辑:获取请求头使用请求方法获取请求上下文,获取请求头后再去获取token,把token加到请求头

里面,谁要使用就让谁依赖feign拦截模块

/**
 * feign 配置文件
 * <p>
 * 将请求头中的参数,全部作为 feign 请求头参数传递
 */
public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {

    @Value("${spring.profiles.active}")
    private String springProfilesActive;

    @Override
    public void apply(RequestTemplate requestTemplate) {
        HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
        if (request == null) {
            return;
        }
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                //跳过 Accept-Encoding和content-length
                if ("Accept-Encoding".equalsIgnoreCase(name) || "content-length".equalsIgnoreCase(name)) {
                    continue;
                }
                String values = request.getHeader(name);
                requestTemplate.header(name, values);
            }
        }
        //开发环境添加用户请求头,mock用户数据
        if ("dev".equals(springProfilesActive)) {
            String userStr = "{\"userId\":\"1\",\"user_name\":\"admin\",\"authorities\":[\"admin\"]}";
            requestTemplate.header("user", userStr);
        }
    }
}

5.重试机制

失败的情况:

在我们捕获异常并解决异常后,可以调用 JobExecutionException#setRefireImmediately(true) 立即重新执行作业。

try {
    // 抛出异常
} catch (Exception e) {
    JobExecutionException e2 = new JobExecutionException(e);
    // true 表示立即重新执行作业
    e2.setRefireImmediately(true);
    throw e2;
}

错过触发点没有执行的情况,对应的处理规则

CronTrigger
withMisfireHandlingInstructionDoNothing
——不触发立即执行
——等待下次Cron触发频率到达时刻开始按照Cron频率依次执行
withMisfireHandlingInstructionIgnoreMisfires
——以错过的第一个频率时间立刻开始执行
——重做错过的所有频率周期后
——当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行
withMisfireHandlingInstructionFireAndProceed
——以当前时间为触发频率立刻触发一次执行
——然后按照Cron频率依次执行

SimpleTrigger
withMisfireHandlingInstructionFireNow
——以当前时间为触发频率立即触发执行
——执行至FinalTIme的剩余周期次数
——以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
——调整后的FinalTime会略大于根据starttime计算的到的FinalTime值

6.线程管理

调度中心集成很多业务系统后,难免会有很多任务集中执行,此时应该在线程管理池中进行管理,超过一定数量的线程执行任务,过来的线程执行设置为等待执行

通过对调取器的调度原理的分析,我们可以知道:当正在执行的调度任务个数超过了调度器中设置的最大值时,就会出线程阻塞,调度任务延迟执行的现象,所以就只要修改这个线程池中线程的最大个数就行了。

// 执行任务的线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "10");
prop.put("org.quartz.threadPool.threadPriority", "5");

二:系统流程图

三:配置文件

Properties prop = new Properties();
// 调度标识名 集群中每一个实例都必须使用相同的名称
prop.put("org.quartz.scheduler.instanceName", "quartz-job");
// ID设置为自动获取 每一个必须不同
prop.put("org.quartz.scheduler.instanceId", "AUTO");
// 数据保存方式为持久化
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
// 数据库方言
prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
// 表前缀
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
// 是否启用集群功能
prop.put("org.quartz.jobStore.isClustered", "true");
// 设置一个频度(毫秒),用于实例报告给集群中的其他实例。这会影响到侦测失败实例的敏捷度。它只用于设置了 isClustered 为 true 的时候。
prop.put("org.quartz.jobStore.clusterCheckinInterval", "20000");
// 这是 JobStore 能处理的错过触发的 Trigger 的最大数量。处理太多(超过两打) 很快会导致数据库表被锁定够长的时间,这样就妨碍了触发别的(还未错过触发) trigger 执行的性能。
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
// 当前时间与下一次执行的时间差大于改值时认为missFire(错过触发),根据missFire原则处理任务;若小于该值直接执行任务。默认60000(60秒),此参数设置要小于定时任务的最间隔小时间
prop.put("org.quartz.jobStore.misfireThreshold", "5000");
// 值为 True 时告诉 Quartz (当使用 JobStoreTX 或 CMT 时),调用 JDBC 连接的 setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) 方法,设置事务隔离级别为串行
// 这能助于防止某些数据库在高负荷和长事物时的锁超时。
prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
// 执行任务的线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "10");
prop.put("org.quartz.threadPool.threadPriority", "5");
// 指定Quartz生成的线程是否继承初始化线程的上下文类加载器
prop.put("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "true");
// 跳过更新检查
prop.put("org.quartz.scheduler.skipUpdateCheck", true);
prop.put("org.quartz.plugin.shutdownhook.class", "org.quartz.plugins.management.ShutdownHookPlugin");
prop.put("org.quartz.plugin.shutdownhook.cleanShutdown", "true");

四:核心类

/**
 * 创建quartz定时任务
 *
 * @param scheduler      调度器
 * @param jobClass       任务class,必须继承QuartzJobBean
 * @param cronExpression cron表达式
 * @param jobDataMap     任务信息
 */
public static void createJob(Scheduler scheduler, String name, String group, Class<? extends QuartzJobBean> jobClass, String cronExpression, JobDataMap jobDataMap) {
    //任务所属分组
    LOG.info("----createJob start ,name:{},group:{}", name, group);
    if (exist(scheduler, name, group)) {
        LOG.info("----createJob fail ,job already existed,name:{},group:{}", name, group);
        return;
    }
    //创建任务
    JobDetail jobDetail;
    if (jobDataMap != null) {
        //requestRecovery(true)指在集群中,一个scheduler执行job失败,将会被另外一个scheduler执行
        jobDetail = newJob(jobClass).withIdentity(name, group).usingJobData(jobDataMap).requestRecovery(true).build();
    } else {
        jobDetail = newJob(jobClass).withIdentity(name, group).requestRecovery(true).build();
    }
    //创建任务触发器
    Trigger trigger;
    if (StringUtils.isBlank(cronExpression)) {
        Date startTime = DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND);
        trigger = newTrigger().withIdentity(name, group).startAt(startTime).build();
    }else{
        //cron表达式封装
        //missfire处理 withMisfireHandlingInstructionDoNothing 错过触发时间时,不执行执行的,等待下一个执行时间
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression).withMisfireHandlingInstructionDoNothing();
        trigger = newTrigger().withIdentity(name, group).withSchedule(scheduleBuilder).build();
    }
    //将触发器与任务绑定到调度器内
    try {
        Date firstFireTime = scheduler.scheduleJob(jobDetail, trigger);
        LOG.info("----createJob success,firstFireTime:{}", DateFormatUtils.format(firstFireTime, "yyyy-MM-dd HH:mm:ss"));
    } catch (SchedulerException e) {
        LOG.error("-----createJob exception", e);
    }
}

/**
 * 移除一个任务
 * @param scheduler
 * @param jobName
 * @param jobGroupName
 * @param triggerName
 * @param triggerGroupName
 */

public static void close(Scheduler scheduler, String jobName, String jobGroupName,
                         String triggerName, String triggerGroupName) {
    try {
        TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
        // 停止触发器
        scheduler.pauseTrigger(triggerKey);
        // 移除触发器
        scheduler.unscheduleJob(triggerKey);
        // 删除任务
        scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));
        LOG.info("----quartzJob success");
    } catch (SchedulerException e) {
        LOG.error("----quartzJob close exception", e);
    }
}

/**
 * @Description: 修改一个任务的触发时间
 * @param triggerName
 * @param triggerGroupName
 * @param cron
 */
public static void modifyJobTime(Scheduler scheduler, String triggerName, String triggerGroupName, String cron) {
    TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
    try {
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        if (trigger == null) {
            return;
        }
        String oldTime = trigger.getCronExpression();
        if (!oldTime.equalsIgnoreCase(cron)) {
            // 触发器
            TriggerBuilder<Trigger> triggerBuilder = newTrigger();
            // 触发器名,触发器组
            triggerBuilder.withIdentity(triggerName, triggerGroupName);
            triggerBuilder.startNow();
            // 触发器时间设定
            triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
            // 创建Trigger对象
            trigger = (CronTrigger) triggerBuilder.build();
            // 修改一个任务的触发时间
            scheduler.rescheduleJob(triggerKey, trigger);

        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}


/**
 * 暂停一个job
 *
 * @param scheduler
 * @throws SchedulerException
 */
public static void pauseJob(Scheduler scheduler, String jobName, String jobGroupName) {
    JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
    try {
        scheduler.pauseJob(jobKey);
    } catch (SchedulerException e) {
        e.printStackTrace();
    }

}

/**
 * @Description:恢复一个任务
 * @param jobName
 * @param jobGroupName
 */
public static void resumeJob(Scheduler scheduler, String jobName, String jobGroupName) {
    JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
    try {
        scheduler.resumeJob(jobKey);
    } catch (SchedulerException e) {
        e.printStackTrace();
    }
}

public static Trigger.TriggerState getJobStatus(Scheduler scheduler, String name, String group) {
    TriggerKey triggerKey = new TriggerKey(name, group);
    Trigger.TriggerState triggerState = null;
    try {
        triggerState = scheduler.getTriggerState(triggerKey);
    } catch (SchedulerException e) {
        e.printStackTrace();
    }
    return triggerState;
}


/**
 * 判断任务是否存在
 *
 * @param scheduler 调度器
 * @param name      任务名称
 * @param group     任务分组
 * @return true==存在 false==不存在
 */
public static boolean exist(Scheduler scheduler, String name, String group) {
    JobDetail jobDetail;
    JobKey jobKey = new JobKey(name, group);
    TriggerKey triggerKey = new TriggerKey(name, group);
    try {
        jobDetail = scheduler.getJobDetail(jobKey);
        if (jobDetail != null) {
            if (Trigger.TriggerState.ERROR.equals(scheduler.getTriggerState(triggerKey))) {
                LOG.info("-----定时任务状态异常,恢复状态,job name :{}", name);
                scheduler.resumeJob(jobKey);
            }
            return true;
        }
    } catch (SchedulerException e) {
        LOG.error("-----exist exception", e);
    }
    return false;
}

/**
 * 获取定时任务列表
 *
 * @param scheduler 任务调度器
 * @return
 */
public static List<JobView> jobs(Scheduler scheduler) {
    List<JobView> jobViews = Lists.newArrayList();
    try {
        for (String groupName : scheduler.getJobGroupNames()) {
            for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
                String jobName = jobKey.getName();
                String jobGroup = jobKey.getGroup();

                // name and group
                JobView jobView = new JobView();
                jobView.setName(jobName);
                jobView.setGroup(jobGroup);

                // params
                JobDetail qJobDetail = scheduler.getJobDetail(jobKey);
                if (null != qJobDetail.getJobDataMap()) {
                    Map<String, Object> params = Maps.newHashMap();
                    params.putAll(qJobDetail.getJobDataMap());
                    jobView.setParams(params);
                }
                List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                // 应该只有一个
                for (Trigger trigger : triggers) {
                    trigger.getNextFireTime();
                    if (trigger instanceof CronTrigger) {
                        CronTrigger cronTrigger = (CronTrigger) trigger;
                        String cronExpr = cronTrigger.getCronExpression();
                        jobView.setCronExpression(cronExpr);
                    }
                    jobView.setNextFireTime(DateUtils.formatDate(trigger.getNextFireTime(), "yyyy-MM-dd HH:mm:ss"));
                    Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
                    jobView.setStatus(state.name());
                }
                jobViews.add(jobView);
            }
        }
    } catch (Exception e) {
        return Collections.emptyList();
    }
    return jobViews;

}
}

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值