時限タスク分散スケジューラを自分で設計する方法

創造し続け、成長を加速!「ナゲッツデイリー新プラン・10月アップデートチャレンジ」参加3日目、イベント詳細はこちら

分散スケジューラを使用する理由

分散スケジューラは、主にシステム内のいくつかのタスクのタイミング スケジューリングに使用されます。通常は時限タスクを設計しますが、最も簡単な方法は@scheduled時限タスクをアノテーションで直接設定することで、開発作業も簡単になります。しかし、本番環境で発生した場合、再起動せずにスケジュールされたタスクの時間を変更する必要があるか、何らかの理由で特定のスケジュールされたタスクを閉じる必要がある場合、現時点では動的にすることはできません. 分散スケジューラは、これらの困難な問題を非常にうまく解決できます。

一部の人々は、次のように尋ねるかもしれません: 現在、いくつかの人気のあるオープン ソース スケジューラがあります。たとえばxxl-job、独自のスケジューラを設計する必要があるのはなぜですか。実際、オープンソースの設計が悪いとは言えません.その理由は、その機能が完璧すぎるからです.うまく使用すると、誰かが運用と保守の専門家を必要とします.機能が強力すぎて、.ほとんどの機能は味気ないので、単純なスケジューリング サービスのセットを開発しましたが、それでも必要な場合があります。

分散スケジューリング プロセス

まず、分散スケジューラはデータベース構成に依存する必要があり、主にスケジューリング サービス インターフェイスとスケジューリング時間を構成します。jobスケジューリング サービス クラスタを介してデータベース構成を取得し、スケジュールする必要があるタスクを分析します。ジョブ サービスのクラスタであるため (単一のマシンに展開することもできます)、複数のサービスを防止するためにロックも考慮する必要があります。タスクを同時に複数回スケジュールすることから。最後jobのサービスは、サービス インターフェイスを解析し、トリガー時点が検出されると、アプリケーション サービス インターフェイスのタスク スケジューリングを開始します。画像.png

分散スケジューリングの詳細な設計分析

データベース設計

Job_info テーブルの設計: 主にいくつかのジョブ タスクの構成を記録します. 主なフィールドを分析しましょう:

  • job_cron: スケジュールされたタスクのトリガー時間の構成
  • config_id: 関連付けられたスケジューリング タスク サービス インターフェイス構成の主キー
  • execute_timeout: タスク スケジューリングのタイムアウト設定。スケジューリング時間が長すぎて結果が得られないようにするため
  • execute_fail_retry_count: タスクのスケジューリングが失敗した場合の再試行回数
  • job_status: スケジュール タスク ステータス スイッチ構成
  • trigger_last_time:最后一次调度时间
  • trigger_next_time:下一次执行时间

画像.png

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

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

画像.png

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);
复制代码

案例配置说明

  • 添加两个定时任务配置,此配置如有需要也可开发个简单的页面方便配置添加与更改。 画像.png

  • 定时任务对应的调度服务接口配置 画像.png

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

总结

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

おすすめ

転載: juejin.im/post/7149817451960598559