如何自己设计一个定时任务分布式调度器

为什么要使用分布式调度器

分布式调度器主要应用于系统中一些任务定时调度处理。通常我们设计一个定时任务,最简单的就是直接使用@scheduled注解配置好定时任务,这样开发工作也简单。但是也许会有一种情况,如果发生在生产环境上,需要不重启就去变更定时任务时间,或者可能由于某些原因我们需要关闭某个定时任务,那么这时候就无法做到动态化。分布式调度器就能很好的解决这些疑难杂症。

有的人可能会问:现在开源的调度器也有一些很流行的,比如xxl-job,为什么还要自己设计一套。其实我们也不能说开源的设计不好,原因是它的功能太完善,如果要用好还要有人专门运维处理,功能过于强大,大部分功能都是鸡肋,所以自研一套简单的调度服务有些时候还是很有必要的。

分布式调度流程

首先分布式调度器需要依赖数据库配置,主要配置调度服务接口和调度时间。通过调度服务集群获取数据库配置,解析完需要进行调度的任务,由于是job服务的一个集群(也可以单机部署)所以也要考虑到加锁,防止多个job服务同时对一个任务多次调度。最终job服务将解析完的服务接口,检测到触发时间点就对应用服务接口发起任务调度。

分布式调度细节设计分析

数据库设计

job_info表设计:主要记录一些job任务的配置,下面分析一下主要字段:

  • job_cron:定时任务触发时间配置
  • config_id:关联调度任务服务接口配置主键
  • execute_timeout:任务调度超时时间配置,防止调度时间过长无结果
  • execute_fail_retry_count:如果任务调度失败重试次数
  • job_status:调度任务状态开关配置
  • trigger_last_time:最后一次调度时间
  • trigger_next_time:下一次执行时间

job_config表设计:主要配置一些任务对应需要调度的服务接口信息。

  • execute_servier:所需调度的应用服务
  • execute_method:调度应用服务接口
  • execute_param:调度参数配置
  • service_type:服务类型(GET/POST)


job服务调度流程设计

  • 读取配置:首先job服务需要不断的读取数据库配置,从而得知有哪一些任务需要进行调度。可以通过一个while循环加上休眠一段时间不断读取配置,下面就用简短的伪代码做个思路分析:
while(true) {
    // PRE_READ_TIME每次刷新时间间隔
    TimeUnit.MILLISECONDS.sleep(PRE_READ_TIME - System.currentTimeMillis() % 1000);
    // 读取配置,给定一个时间,获取这段时间内要执行调度的任务以及初次配置的任务trigger_next_time=0
    List<JobInfo> jobInfos = jobInfoMapper.select(time);
    // 循环对任务一一进行解析
    // 1.job应用对获取到的任务进行加锁,防止job集群其他服务同时调用,如果确定只会有单机部署可不加锁
    int resultCount = jobInfoMapper.updateByOptimisticLock(jobInfo);
    // 2.加锁成功继续执行下一步,对首次配置的任务(trigger_next_time=0)需要获取job_cron进行解析,计算出真实的下次执行时间trigger_next_time
     refreshNextValidTime(jobInfo, new Date(nowTime));
    // 3.即将要执行的任务加入队列
   checkHighFrequency(jobInfo, nowTime);
}

private void checkHighFrequency(JobInfo jobInfo, Long nowTime) throws ParseException {
    // PRE_READ_TIME = 5000,即提前预留5秒,将任务加入队列
    if (jobInfo.getTriggerNextTime() < (nowTime + PRE_READ_TIME)) {
        // 将任务放入待执行队列
        triggerPoolHelper.triggerJob(jobInfo, jobInfo.getTriggerNextTime() - nowTime);
        // 任务加入队列后,再次更新计算下次调度job的时间
        refreshNextValidTime(jobInfo, new Date(jobInfo.getTriggerNextTime()));
        // 判断是否是超高频繁任务,即调度周期小于5s一次
        checkHighFrequency(jobInfo, nowTime);
    }
}
// 计算下次执行时间
private void refreshNextValidTime(JobInfo jobInfo, Date fromTime) throws ParseException {
    // 时间表达式转换计算下次触发时间
    Date nextValidTime = new CronExpression(jobInfo.getJobCron()).getNextValidTimeAfter(fromTime);
    if (nextValidTime != null) {
        jobInfo.setTriggerLastTime(jobInfo.getTriggerNextTime());
        jobInfo.setTriggerNextTime(nextValidTime.getTime());
    }
}

  • 线程池队列执行任务调度
public void triggerJob(JobInfo jobInfo, long delay) {
    JobInfo copyOf = new JobInfo();
    BeanUtils.copyProperties(jobInfo, copyOf);
    JobTriggerThread triggerThread = new JobTriggerThread(copyOf, tinyJobExecutor.get(jobInfo.getJobType()));
    // 小于0说明是延期的任务,立即执行
    if (delay <= 0) {
        // 加入线程池
        triggerPool.execute(triggerThread);
    }
    // 大于0说明还未到调度时间,延迟调度
    else {
        triggerPool.schedule(triggerThread, delay, TimeUnit.MILLISECONDS);
    }
}

  • 任务调度流程:SpringCloud服务使用DiscoveryClient,根据job_config表配置的服务名获取集群服务列表,再根据随机(或自定义算法)计算获取一个服务实例,用该实例创建请求并发起服务接口调用,最终再根据调用结果进行日志记录,以及失败后续是否进行重试处理。
List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances(jobConfig.getExecuteService());
// 随机获取服务列表(可自定义算法)
ServiceInstance serviceInstance = getRandomInstrance(serviceInstanceList);
CloseableHttpClient httpClient = HttpClients.createDefault();
// 创建请求
HttpPost httpPost = new HttpPost(serviceInstance.getUri() + "/" + jobConfig.getExecuteMethod() + "?" + jobConfig.getExecuteParam());
// http发起调用
CloseableHttpResponse response = httpClient.execute(httpPost);

案例配置说明

  • 添加两个定时任务配置,此配置如有需要也可开发个简单的页面方便配置添加与更改。
  • 定时任务对应的调度服务接口配置

根据以上的配置,定时刷新获取任务列表,任务首次配置trigger_next_time=0,需解析成具体执行时间点,任务调度判断该时间点是否达到可执行时间,在达到指定时间点job服务将对该接口发起调用并记录调度日志。

总结

使用分布式调度器能够很好的管理我们的定时任务接口,开发人员也只需专注开发业务接口,让业务与配置完全分离。定时配置还可以根据业务场景统一进行时间协调管理,以免在有些时间点多任务同时处理,可以将时间配置的分散点以减轻CPU的压力。如果系统业务量少,定时任务也不多的情况也没必要多浪费时间开发一个调度系统。

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

推荐阅读更多精彩内容