Java タイミング タスクのソリューション (Quartz など)

①JDKタイマー:タイマー

理論的根拠: タイム ホイール アルゴリズム

  • リンクされたリストまたは配列はタイム ホイールを実装します: while-true-sleep

    配列をトラバースし、添え字ごとに連結リストを配置し、連結リスト ノードにタスクを配置し、トラバーサルに達したときに取り出して実行する

    欠点: 時間を表すために配列の長さが 12 であると仮定すると、13 時に実行したいのですが、これはあまり便利ではありません。

  • ラウンド型タイムホイール:タスクにラウンドが記録され、通過するとラウンドが1つ減り、0になると取り出されて実行される

    すべてのタスクをトラバースする必要があり、効率が低い

  • 階層的なタイム ホイール: 異なる次元の複数のホイールを使用

    Tianlun: いつ実行するかを記録する

    月の輪: 印の実行を記録する

    ムーンホイールはトラバースされており、タスクを取り外してスカイホイールに配置できるため、特定の日時に実行できます

/**
 * @author cVzhanshi
 * @create 2022-11-02 20:27
 */
public class TimerTest {
    
    
    public static void main(String[] args) {
    
    
        Timer timer = new Timer();  // 任务启动
        for (int i = 0; i < 2; i++) {
    
    
            TimerTask timerTask = new FooTimerTask("cvzhanshi" + i);
            // 任务添加 第二个参数,启动时间   第三个参数,执行间隔时间
            timer.schedule(timerTask , new Date() , 2000);
            // 预设的执行时间nextExecutTime 12:00:00   12:00:02  12:00:04
            //schedule  真正的执行时间 取决上一个任务的结束时间  ExecutTime   03  05  08  丢任务(少执行了次数)
            //scheduleAtFixedRate  严格按照预设时间 12:00:00   12:00:02  12:00:04(执行时间会乱)
            //单线程  任务阻塞  任务超时
        }
    }
}

ソースコードの解析を実行する

// Timer类中的两个常量
// 任务队列
private final TaskQueue queue = new TaskQueue();
// 执行任务的线程
private final TimerThread thread = new TimerThread(queue);
  1. 新しいタイマー();

    // 进入构造器
    public Timer() {
          
          
        // 调用了另一个构造器
        this("Timer-" + serialNumber());
    }
    
    public Timer(String name) {
          
          
        thread.setName(name);
        // 启动线程执行线程的run方法
        thread.start();
    }
    
  2. スレッドの run() メソッド

    public void run() {
          
          
        try {
          
          
            // 主要方法 后面再说
            mainLoop();
        } finally {
          
          
          ...
        }
    }
    
  3. timer.schedule(timerTask、新しい日付()、2000);

    public void schedule(TimerTask task, Date firstTime, long period) {
          
          
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), -period);
    }
    
    
    private void sched(TimerTask task, long time, long period) {
          
          
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");
    
        // 充分限制周期值,以防止数字溢出,同时仍然有效地无限大。
        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;
    
        
        synchronized(queue) {
          
          
            if (!thread.newTasksMayBeScheduled)
                throw new IllegalStateException("Timer already cancelled.");
    
            synchronized(task.lock) {
          
          
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                    "Task already scheduled or cancelled");
                // 设置下次执行时间
                task.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }
    		// 加入任务队列
            queue.add(task);
            // 获取最近要执行的任务,如果是当前任务,唤醒队列
            if (queue.getMin() == task)
                queue.notify();
        }
    }
    
  4. mainLoop() 主な実行ステップ

    private void mainLoop() {
          
          
        while (true) {
          
          
            try {
          
          
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
          
          
                    // 等待队列变为非空
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // 队列是空的,将永远保留;死亡
    
                    // 队列非空;获取第一个任务去执行
                    long currentTime, executionTime;
                    // 获取任务
                    task = queue.getMin();
                    synchronized(task.lock) {
          
          
                        // 如果任务状态是取消,无需操作,再次轮询队列
                        if (task.state == TimerTask.CANCELLED) {
          
          
                            queue.removeMin();
                            continue; 
                        }
                        // 当前时间
                        currentTime = System.currentTimeMillis();
                        // 下次执行的时间
                        executionTime = task.nextExecutionTime;
                        // 如果下次执行时间小于等于当前时间
                        if (taskFired = (executionTime<=currentTime)) {
          
          
                            // 如果任务是单次的,直接删除
                            if (task.period == 0) {
          
           
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else {
          
           
                                // 重复任务,重新安排,计算下次执行时间
                                queue.rescheduleMin(
                                    task.period<0 ? currentTime   - task.period
                                    : executionTime + task.period);
                            }
                        }
                    }
                    //任务尚未启动;等待
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)
                    // 执行任务,注意是单线程运行,不是运行start方法运行的
                    task.run();
            } catch(InterruptedException e) {
          
          
            }
        }
    }
    

概要: タイマーは、問題のあるシングルスレッド タスク ブロック タスク タイムアウトです

  • TaskQueue: timeTask を格納する小さなトップ ヒープ
  • TimerThread: タスク実行スレッド
    • 実行する必要のあるタスクが存在するかどうかを常にチェックし、存在する場合は実行する無限ループ
    • まだこのスレッドで実行されます
  • タスクのシングルスレッド実行、タスクが互いにブロックする可能性がある
    • スケジュール: タスクの実行タイムアウトにより、後続のタスクが押し戻され、指定された時間内の実行数が減少します
    • scheduleAtFixedRate: タスクのタイムアウトにより、次のタスクが中間間隔なしですぐに実行される場合があります
  • 実行時例外により、タイマー スレッドが終了します
  • タスクのスケジューリングは絶対時間に基づいており、システム時間に敏感です

②タイミングタスクスレッドプール

テストコード

/**
 * @author cVzhanshi
 * @create 2022-11-03 15:05
 */
public class ScheduleThreadPoolTest {
    
    

    public static void main(String[] args) {
    
    
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        for (int i = 0; i < 2; i++){
    
    
            // 第一个参数:任务
            // 第二个参数:第一次执行的时间
            // 第三个参数:下次任务间隔的时间
            // 第四个参数:时间单位
            scheduledThreadPool.scheduleAtFixedRate(new Task("task-" + i ),0,2, TimeUnit.SECONDS);
            
            // 执行单次任务的api
            // scheduledThreadPool.schedule(new Task("task-" + i ),0, TimeUnit.SECONDS);
        }
    }
}
class Task implements Runnable{
    
    

    private String name;

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

    public void run() {
    
    
        try {
    
    
            System.out.println("name="+name+",startTime=" + new Date());
            Thread.sleep(3000);
            System.out.println("name="+name+",endTime=" + new Date());

            //线程池执行
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

ScheduledThreadPoolExecutor

  • 複数のスレッドを使用して、互いにブロックすることなくタスクを実行する
  • スレッドが失敗すると、タスクを実行するために新しいスレッドが作成されます
    • スレッドが例外をスローした場合、タスクは破棄され、キャプチャする必要があります
  • DelayedWorkQueue: 小さなトップ ヒープ、無制限のキュー
    • 時限スレッドプールでは、スレッドの最大数は無意味です
    • 実行時間が現在時刻に近いタスクがキューの先頭にある
    • ScheduleFutureTask を追加するために使用されます (Future を継承し、RunnableScheduledFuture インターフェースを実装します)。
    • スレッド プール内のスレッドは、DelayQueue から ScheduleFutureTask を取得し、タスクを実行します。
    • Delayd インターフェイスを実装します。getDelay メソッドで遅延時間を取得できます。
  • リーダーフォロワーモード
    • 実行を待っているタスクがたくさんあり (通常はキューに格納され、並べ替えられます)、すべてのワーカー スレッドの 1 つだけがリーダー スレッドであり、他のスレッドはフォロワー スレッドであるとします。リーダー スレッドのみがタスクを実行できますが、残りのフォロワー スレッドはタスクを実行せず、休止状態になります。リーダー スレッドがタスクを取得すると、タスクを実行する前にフォロワー スレッドになり、同時に新しいリーダー スレッドが選択されてタスクが実行されます。この時点で次のタスクがあれば、それが新しいリーダー スレッドとして実行され、このプロセスが繰り返されます。前にタスクを実行していたスレッドが実行を終了して戻ってくると、その時点でタスクが存在しない場合、またはタスクはあるがリーダー スレッドとして他のスレッドが存在する場合は、それ自体がスリープすると判断します。タスクはあるが、この時点のスレッドにリーダーがない場合、タスクを実行するためにリーダー スレッドに再度なります。
    • 不要なウェイクアップとブロッキング操作を避ける
  • 適用シナリオ
    • 複数のバックグラウンド スレッドが周期的なタスクを実行するのに適しています。また、リソース管理のニーズを満たすためにバックグラウンド スレッドの数を制限する必要があります。

SingleThreadScheduledExecutor

  • シングルスレッドの ScheduledThreadPoolExecutor
  • アプリケーション シナリオ: 単一のバックグラウンド スレッドが定期的なタスクを実行するのに適しています。同時に、タスクが順次実行されるようにする必要があります。

③タイミングフレームクオーツ

学習資料 1

学習資料 2

3.1 はじめに

Quartz は、ジョブ スケジューリングの分野における OpenSymphony オープン ソース組織のもう 1 つのオープン ソース プロジェクトであり、2EE および |2SE アプリケーションと組み合わせて使用​​することも、単独で使用することもできます。

Quartz は、豊富な機能を備えたオープン ソースの「タスク スケジューリング ライブラリ」であり小さな独立したアプリケーションから大規模な電子商取引システムまで、あらゆる Java アプリケーションに統合できますQuartz は、単純なスケジュールと複雑なスケジュールを作成して、数十、数百、さらには数万のタスクを実行できます。タスク ジョブは、任意の機能を実行できる標準 Java コンポーネントとして定義されます。Quartz スケジューリング フレームワークには、JTA トランザクションやクラスター サポートなど、多くのエンタープライズ レベルの機能が含まれています。

つまり、Quartz は Java ベースのタスク スケジューリング フレームワークであり、任意のタスクを実行するために使用されます。

Quartz に関わる設計パターン

●ビルダーモード

●ファクトリーモード

●コンポーネントモードのJobDetailトリガー

●連鎖プログラミング

3.2 コアコンセプトとアーキテクチャ

  • タスク ジョブ

    Job は実装したいタスククラスです. 各 Job は org.quartz.job インターフェイスを実装する必要があり, インターフェイスによって定義された execute() メソッドのみを実装する必要があります.

  • トリガートリガー

    トリガーは、タスクを実行するためのトリガーです. たとえば、毎日 3 時に統計メールを送信する場合、トリガーは 3 時にタスクを実行するように設定します. トリガーには、主に SimplerTrigger と CronTrigger の 2 種類があります。

  • スケジューラースケジューラー

    スケジューラは、タスクジョブとトリガートリガーを統合したタスクのスケジューラーであり、トリガーによって設定された時間に基づいてジョブを実行します。

クォーツ アーキテクチャ

3.3 一般的に使用されるコンポーネント

以下は、Quartz の重要なコンポーネントでもある Quartz プログラミング API のいくつかの重要なインターフェースです。

  • スケジューラ - スケジューラとやり取りするためのメイン API。
  • ジョブ - タスク コンポーネントをスケジューラによって実装するインターフェイス
  • JobDetail - ジョブの定義に使用されるインスタンス。
  • トリガー - 特定のジョブを実行するための計画を定義するコンポーネント。
  • JobBuilder - JobDetail インスタンスを定義/構築するために使用され、ジョブのインスタンスを定義するために使用されます。
  • TriggerBuilder - トリガー インスタンスの定義/構築に使用されます。
  • スケジューラのライフ サイクルは、SchedulerFactory がそれを作成したときに始まり、Scheduler が shutdown() メソッドを呼び出したときに終了します; スケジューラが作成された後、ジョブとトリガーを追加、削除、および列挙し、他のスケジューリング関連の操作を実行できます (トリガーの一時停止など)。ただし、Scheduler が実際にトリガーをトリガーする (つまり、ジョブを実行する) のは、start() メソッドを呼び出した後でのみです。

3.4 入門デモ

  • スプリング ブート プロジェクトを作成する

  • 依存関係のインポート

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    
  • HelloJob の作成

    /**
     * @author cVzhanshi
     * @create 2022-11-02 18:35
     */
    public class HelloJob implements Job {
          
          
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
          
          
            System.out.println("HelloJob" + new Date());
        }
    }
    
  • メイン メソッドで構成タスクを呼び出す

    /**
     * @author cVzhanshi
     * @create 2022-11-02 18:36
     */
    public class HelloSchedulerDemo {
          
          
        public static void main(String[] args) throws SchedulerException {
          
          
            // 1.调度器(Scheduler),从工厂获取调度实例
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //2.任务实例(JobDetail)
            JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)   //加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
                    .withIdentity("job1", "group1")      //参数1:任务的名称(唯一实例),参数2:任务组的名称
                    .build();
            //3.触发器(Trigger)
            SimpleTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "group2")  //参数1:触发器的名称(唯一实例),参数2:触发器组的名称
                    .startNow() // 马上启动触发器
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5))// 每5s触发一次,一直执行
                    .build();
            //让调度器关联任务和触发器,保证按照触发器定义的条件执行任务
            scheduler.scheduleJob(jobDetail, trigger);
    
            scheduler.start();
        }
    }
    
  • の結果

    HelloJobThu Nov 03 16:54:05 CST 2022 //每五秒执行一次
    
    HelloJobThu Nov 03 16:54:10 CST 2022
    
    HelloJobThu Nov 03 16:54:15 CST 2022
    

3.5 Job と JobDetail

public static void main(String[] args) throws SchedulerException {
    
    
    // 1.调度器(Scheduler),从工厂获取调度实例
    Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
    //2.任务实例(JobDetail)
    JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)   //加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
        .withIdentity("job1", "group1")      //参数1:任务的名称(唯一实例),参数2:任务组的名称
        .build();
    //3.触发器(Trigger)
    SimpleTrigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger1", "group2")  //参数1:触发器的名称(唯一实例),参数2:触发器组的名称
        .startNow() // 马上启动触发器
        .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5))
        .build();
    //让调度器关联任务和触发器,保证按照触发器定义的条件执行任务
    scheduler.scheduleJob(jobDetail, trigger);

    scheduler.start();
}

私たちはデモを通して知っています。JobDetail を作成するときに、実行するジョブのクラス名を JobDetail に渡すため、スケジューラに JobDetail インスタンスを渡します。これにより、スケジューラは実行することを認識します。

実行するジョブのタイプ。スケジューラがジョブを実行するたびに、execute(...) メソッドを呼び出す前に、クラスの新しいインスタンスが作成されます。実行後、インスタンスへの参照は破棄され、インスタンスゴミになります

ガベージ コレクション。この実行戦略の結果の 1 つは、ジョブに引数のないコンストラクタが必要であるということです(デフォルトの JobFactory を使用する場合)。別の結果は、ジョブ クラスでステートフル データを定義してはならないということです。

これらのプロパティの値は、ジョブの複数回の実行で保持されないためです

では、ジョブ インスタンスに属性や構成を追加するにはどうすればよいでしょうか。ジョブの複数回の実行中にジョブのステータスを追跡する方法は?

  • ジョブデータマップ

ジョブデータマップ

JobDataMap には無制限の (シリアル化された) データ オブジェクトを含めることができ、その中のデータはジョブ インスタンスの実行時に使用できます。JobDataMap は Java Map インターフェイスの実装であり、追加の

プリミティブ型のデータへのアクセスを容易にするメソッド。

ジョブをスケジューラに追加する前に、JobDetail を作成するときに、コードに示すように、データを JobDataMap に入れることができます。

//创建一个job
JobDetail job = JobBuilder.newJob(HelloJob.class)
    .usingJobData("j1", "jv1")
    .withIdentity("myjob", "mygroup")
    .usingJobData("jobSays", "Hello World!")
    .usingJobData("myFloatValue", 3.141f)
    .build();

次の例に示すように、ジョブの実行中に JobDataMap からデータを取得できます。

public class HelloJob implements Job {
    
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
    
    
        Object tv1 = context.getTrigger().getJobDataMap().get("t1");
        Object tv2 = context.getTrigger().getJobDataMap().get("t2");
        Object jv1 = context.getJobDetail().getJobDataMap().get("j1");
        Object jv2 = context.getJobDetail().getJobDataMap().get("j2");
        Object sv = null;
        try {
    
    
            sv = context.getScheduler().getContext().get("skey");
        } catch (SchedulerException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(tv1+":"+tv2);
        System.out.println(jv1+":"+jv2);
        System.out.println(sv);
        System.out.println("hello:"+ LocalDateTime.now());
    }
}

ジョブクラスの JobDataMap に格納されたデータのキーに set メソッドを追加すると (上記の例のように setJobSays(String val) メソッドを追加する)、ジョブが呼び出されたときに Quartz のデフォルトの JobFactory 実装が自動的に呼び出されます。これらの設定メソッドにより、execute() メソッドでマップからデータを明示的にフェッチする必要がなくなります。

/**
 * @author cVzhanshi
 * @create 2022-11-02 18:35
 */
public class HelloJob implements Job {
    
    
    private String j1;

    public void setJ1(String j1) {
    
    
        this.j1 = j1;
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    
    
        
        System.out.println(j1); // jv1
    }
}


JobDetail job = JobBuilder.newJob(HelloJob.class)
    .usingJobData("j1", "jv1")
    .withIdentity("myjob", "mygroup")
    .build();

ジョブが実行されるとき、JobExecutionContext の JobDataMap は非常に便利です。JobDetail 内の JobDataMap と Trigger 内の JobDataMap の和集合ですが、

同じデータで、後者は前者の値を上書きします。

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    
    
    JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();
    System.out.println(mergedJobDataMap.get("j1"));
    System.out.println(mergedJobDataMap.get("t1"));
    System.out.println(mergedJobDataMap.get("name")); // jobDetail中的数据会被trigger覆盖,如果key相同的话
}

ジョブインスタンス

  • ジョブ クラスを 1 つだけ作成してから、ジョブに関連付けられた複数の JobDetail インスタンスを作成できます。各インスタンスには独自のプロパティ セットと JobDataMap があり、最後にすべてのインスタンスをスケジューラに追加します。

  • たとえば、Job インターフェイスを実装するクラス「SalesReportJob」を作成します。ジョブには、販売レポートを担当する営業担当者の名前を表すパラメーター (JobdataMap を介して渡される) が必要です。したがって、「SalesReportForJoe」、「SalesReportForMike」など、複数のジョブのインスタンス (JobDetail) を作成し、「joe」と「mike」を JobDataMap のデータとして、対応するジョブ インスタンスに渡すことができます。

  • トリガーがトリガーされると、関連する JobDetail インスタンスがロードされ、JobDetail によって参照されるジョブ クラスが、Scheduler で構成された JobFactory を介して初期化されます。デフォルトの JobFactory 実装は、単にジョブ クラスの newInstance() メソッドを呼び出してから、JobDataMap 内のキーのセッター メソッドを呼び出そうとします。IOC または DI コンテナーがジョブ インスタンスを作成/初期化できるようにするなど、独自の JobFactory 実装を作成することもできます。

  • Quartz の記述言語では、保存された JobDetail を「ジョブ定義」または「JobDetail インスタンス」と呼び実行中のジョブを「ジョブ インスタンス」または「ジョブ定義インスタンス」と呼びます。「ジョブ」を使用する場合、通常はジョブ定義または JobDetail を指し、Job インターフェイスを実装するクラスを指す場合は、通常「ジョブ クラス」を使用します。

ジョブのステータスと同時実行

  • Scheduler が実行されるたびに、JobDetail に従って新しい Job インスタンスが作成されるため、同時訪問の問題を回避できます (JobDetail のインスタンスも新しいものです)。次のコード例を参照してください

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
          
          
        System.out.println("JobDetail: " + System.identityHashCode(jobExecutionContext.getJobDetail().hashCode()));
        System.out.println("Job: " + System.identityHashCode(jobExecutionContext.getJobInstance().hashCode()));
    }
    
    JobDetail: 2046941357
    Job: 1895412701
    // 每次都不一样
    JobDetail: 1157785854
    Job: 1341784974
    
  • **Quartz のスケジュールされたタスクは、デフォルトで同時に実行され、最後のタスクが実行されるまで待機しません。間隔が満了している限り、タスクは実行されます。スケジュールされたタスクの実行時間が長すぎると、リソースが占有されます。他のタスクがブロックされる原因となります。**同時実行のテスト:

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
          
          
        System.out.println("execute:" + new Date());
        try {
          
          
            Thread.sleep(3000);
        } catch (InterruptedException e) {
          
          
            e.printStackTrace();
        }
    }
    
    // 任务设置成1s执行一次
    // 执行结果 还是每秒执行一次,并没有sleep3s 所以是并发执行
    // execute:Thu Nov 03 17:36:55 CST 2022
    // execute:Thu Nov 03 17:36:56 CST 2022
    // execute:Thu Nov 03 17:36:57 CST 2022 
    

ジョブ状態データ (JobDataMap など) と同時実行性について注意すべき点がいくつかあります。いくつかの注釈をジョブ クラスに追加できます。これらの注釈は、ジョブのステータスと同時実行性に影響します。

  • @DisallowConcurrentExecution: このアノテーションをジョブ クラスに追加して、同じジョブ定義 (ここでは特定のジョブ クラス) の複数のインスタンスを同時に実行しないように Quartz に指示します上記の例を見てください

    たとえば、「SalesReportJob」クラスにこのアノテーションがある場合、同時に実行できる「SalesReportForJoe」インスタンスは 1 つだけですが、「SalesReportForMike」クラスのインスタンスは 1 つだけ同時に実行できます。

    したがって、制限はジョブ クラスではなく、JobDetail に対するものです。しかし、(Quartz を設計する際に) アノテーションをジョブ クラスに配置する必要があると考えています。

    バラエティ。

    コードのテスト

    @DisallowConcurrentExecution
    public class HelloJob implements Job {
          
           
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
          
          
            System.out.println("execute:" + new Date());
            try {
          
          
                Thread.sleep(3000);
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
    // 任务设置成1s执行一次
    // 执行结果 
    // execute:Thu Nov 03 17:36:55 CST 2022
    // execute:Thu Nov 03 17:36:58 CST 2022 
    

実行結果は、次のタスクを実行する前に 3 秒間スリープするため、同じジョブ定義 (ここでは特定のジョブ クラスを指します) の複数インスタンスの同時実行はありません。

  • **@PersistJobDataAfterExecution: このアノテーションをジョブクラスに追加して、ジョブクラスの execute メソッドが (例外なく) 正常に実行された後、JobDetail の JobDataMap のデータを更新するように (トリガーのデータマップには無効)、Quartz に指示します。次回ジョブ(JobDetail)が実行される際、JobDataMap 内の更新データは更新前の古いデータではありません。**@DisallowConcurrentExecution アノテーションと同じですが、アノテーションはジョブ クラスに追加されますが、その制限はジョブ クラスではなく、ジョブ インスタンスに対するものです。ジョブ・クラスのコンテンツがその動作状態に影響を与えることが多いため、注釈はジョブ・クラスによって保持されます(たとえば、ジョブ・クラスのexecuteメソッドはその「状態」を明示的に「理解」する必要があります)。説明: 各実行インスタンスは新しいインスタンスであるため、インスタンス内のデータは新しいものです.このアノテーションを使用すると、前のインスタンスの更新により、次のインスタンスのデータが更新されます.

@PersistJobDataAfterExecution アノテーションを使用する場合は、@DisallowConcurrentExecution アノテーションも使用することを強くお勧めします。

並行して実行すると、JobDataMap に格納されたデータは、競合により不確実になる可能性があります。

ジョブのその他の機能

JobDetail オブジェクトを通じて、ジョブ インスタンスに対して構成できるその他の属性は次のとおりです。

  • 永続性: ジョブが永続的でない場合、アクティブなトリガーが関連付けられていない場合、ジョブはスケジューラから自動的に削除されます。つまり、非永続ジョブの存続期間はトリガーによって決定されます。

    存在するかどうか。

  • RequestsRecovery: ジョブが回復可能で、その実行中にスケジューラがハード シャットダウン (ハード シャットダウン) (実行中のプロセスのクラッシュやシャットダウンなど) を行う場合、

    スケジューラが再起動すると、ジョブが再実行されます。この時点で、ジョブの JobExecutionContext.isRecovering() は true を返します。

3.6 トリガー

トリガーのパブリック プロパティ

すべての種類のトリガーには、トリガーの ID を示す属性 TriggerKey があります。さらに、トリガーには他の多くの公開属性があります。これらの属性は、トリガーの作成時に渡すことができます

トリガービルダーの設定。

トリガーのパブリック プロパティは次のとおりです。

  • jobKey属性:トリガーがトリガーされたときに実行されるジョブの ID

  • startTime属性:トリガーが初めてトリガーされる時刻を設定します。この属性の値は java.util.Date 型で、指定された時点を示します。一部の種類のトリガーは、設定された startTime ですぐにトリガーされます。

    つまり、トリガーです。一部のタイプのトリガーは、トリガーが startTime の後に有効になることを示します。たとえば、今は 1 月で、「毎月 5 日に実行する」というトリガーを設定すると、

    startTime 属性が 4 月 1 日に設定されている場合、トリガーは数か月ぶりにトリガーされます (つまり、4 月 5 日)。

  • endTime属性: トリガーが失敗した時点を示します。たとえば、「毎月 5 日に実行される」トリガーの場合、endTime が 7 月 1 日の場合、最終実行時刻は 6 月 5 日になります。

優先順位

トリガーが多数ある (または Quartz スレッド プール内のワーカー スレッドが少なすぎる) 場合、Quartz はすべてのトリガーを同時にトリガーするのに十分なリソースを持っていない可能性があります; この場合、どのトリガーを最初に使用するかを制御したい場合があります。

Quartz のワーカー スレッドでは、この目的を達成するために、トリガーに優先度属性を設定できます。たとえば、同時にトリガーする必要がある N 個のトリガーがあり、ワーカー スレッドが Z 個しかない場合、優先度が最も高い Z 個のトリガーがトリガーされます。

最初にトリガーします。トリガーに優先順位が設定されていない場合、トリガーは値が 5 のデフォルトの優先順位を使用します。優先順位属性の値は、正または負の任意の整数にすることができます。

  • 注: 同時にトリガーされたトリガーのみが優先度を比較します。10:59 にトリガーされたトリガーは、常に 11:00 にトリガーされたトリガーの前に実行されます。
  • 注: トリガーが回復可能である場合、回復後に再スケジュールされると、優先度は元のトリガーと同じになります。

逃したトリガー

トリガーには重要なミスファイア属性もあります; スケジューラーが閉じている場合、またはジョブを実行するための使用可能なスレッドが Quartz スレッドプールにない場合、永続的なトリガーはそのトリガー時間を逃します (逃します)。

つまり、失火が見逃されます。トリガーのタイプが異なれば、不発のメカニズムも異なります。これらはすべて、デフォルトで「スマート ポリシー」を使用します。これは、トリガーのタイプと構成に従って動作を動的に調整します。いつ

スケジューラーの開始時に、トリガーを逃した (不発) すべての持続トリガーを照会します。次に、それぞれの失火メカニズムに従ってトリガー情報を更新します。

失火判定条件

  • トリガー時間に達したときにジョブが実行されていない
  • 実行された遅延時間が、Quartz によって構成された失火しきい値を超えています

考えられる原因

  • ジョブがトリガー時間に達すると、すべてのスレッドが他のジョブによって占有され、使用可能なスレッドがなくなります
  • ジョブをトリガーする必要がある時点で、スケジューラーが停止します (予期せず停止した可能性があります)。
  • このジョブは @ DisallowConcurrentExecution アノテーションを使用しています.ジョブは同時に実行できません.次のジョブの実行ポイントに到達すると、前のタスクは完了していません.
  • ジョブは過去の開始実行時刻を指定します。たとえば、現在の時刻は 8:00:00 で、指定された開始時刻は 7:00:00 です。

3.7 Spring Boot は Quartz を統合します

  • スプリングブート プロジェクトを作成する

  • 必要な依存関係をインポートする

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.5.7</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>cn.cvzhanshi</groupId>
        <artifactId>test</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>test</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-quartz</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-autoconfigure</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </project>
    
  • 構成ファイル application.yaml を編集します。

    server:
      port: 8889
      datasource:
        url: jdbc:mysql://localhost:3306/test_quartz?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
    
  • Quartz 構成ファイル spring-quartz.properties を編集します

    #============================================================================
    # 配置JobStore
    #============================================================================
    # JobDataMaps是否都为String类型,默认false
    org.quartz.jobStore.useProperties=false
    
    # 表的前缀,默认QRTZ_
    org.quartz.jobStore.tablePrefix = QRTZ_
    
    # 是否加入集群
    org.quartz.jobStore.isClustered = true
    
    # 调度实例失效的检查时间间隔 ms
    org.quartz.jobStore.clusterCheckinInterval = 5000
    
    # 数据保存方式为数据库持久化
    org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
    
    # 数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
    org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    
    #============================================================================
    # Scheduler 调度器属性配置
    #============================================================================
    # 调度标识名 集群中每一个实例都必须使用相同的名称
    org.quartz.scheduler.instanceName = ClusterQuartz
    # ID设置为自动获取 每一个必须不同
    org.quartz.scheduler.instanceId= AUTO
    
    #============================================================================
    # 配置ThreadPool
    #============================================================================
    # 线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
    org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
    
    # 指定线程数,一般设置为1-100直接的整数,根据系统资源配置
    org.quartz.threadPool.threadCount = 5
    
    # 设置线程的优先级(可以是Thread.MIN_PRIORITY(即1)和Thread.MAX_PRIORITY(这是10)之间的任何int 。默认值为Thread.NORM_PRIORITY(5)。)
    org.quartz.threadPool.threadPriority = 5
    
  • ジョブクラスを編集する

    /**
     * @author cVzhanshi
     * @create 2022-11-07 10:20
     */
    @PersistJobDataAfterExecution
    @DisallowConcurrentExecution
    public class QuartzJob extends QuartzJobBean {
          
          
        @Override
        protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
          
          
            try {
          
          
                Thread.sleep(2000);
                System.out.println(context.getScheduler().getSchedulerInstanceId());
                System.out.println("taskname=" + context.getJobDetail().getKey().getName());
                System.out.println("执行时间:" + new Date());
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            } catch (SchedulerException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  • 設定済みスケジューラの統一利用 スケジューラ

    /**
     * @author cVzhanshi
     * @create 2022-11-07 10:53
     */
    @Configuration
    public class SchedulerConfig {
          
          
    
        @Autowired
        private DataSource dataSource;
    
        @Bean
        public Scheduler scheduler() throws IOException {
          
          
            return schedulerFactoryBean().getScheduler();
        }
    
        @Bean
        public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
          
          
            SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
            factoryBean.setSchedulerName("cluster_scheduler");
            // 设置数据源
            factoryBean.setDataSource(dataSource);
            factoryBean.setApplicationContextSchedulerContextKey("application");
            // 加载配置文件
            factoryBean.setQuartzProperties(quartzProperties());
            // 设置线程池
            factoryBean.setTaskExecutor(schedulerThreadPool());
            // 设置延迟开启任务时间
            factoryBean.setStartupDelay(0);
            return factoryBean;
        }
    
        @Bean
        public Properties quartzProperties() throws IOException {
          
          
            PropertiesFactoryBean factoryBean = new PropertiesFactoryBean();
            factoryBean.setLocation(new ClassPathResource("/spring-quartz.properties"));
            factoryBean.afterPropertiesSet();
            return factoryBean.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;
        }
    }
    
  • リスナーを作成し、コンテナーの読み込みイベントをリッスンし、読み込みが完了したらタスクを開始します

    /**
     * @author cVzhanshi
     * @create 2022-11-07 11:04
     */
    @Component
    // 监听springboot容器是否启动,启动了就执行定时任务
    public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
          
          
    
        @Autowired
        private Scheduler scheduler;
    
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
          
          
            TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "group1");
            try {
          
          
                /**
                 * 在调度器中获取指定key的trigger,
                 */
                Trigger trigger = scheduler.getTrigger(triggerKey);
                if(trigger == null){
          
          
                    trigger = TriggerBuilder.newTrigger()
                            .withIdentity("trigger1","group1")
                            .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();
            }
        }
    }
    
  • サービス開始

    # 输出结果
    taskname=job1
    执行时间:Mon Nov 07 14:19:02 CST 2022
    A029095-NC1667801934353
    taskname=job1
    执行时间:Mon Nov 07 14:19:12 CST 2022
    

3.8 クォーツクラスター

Quartz クラスタはノードに割り当てられたジョブ詳細であり、このノードでは変更されません

デモの例:

  • 上記の統合コードを変更します

    /**
     * @author cVzhanshi
     * @create 2022-11-07 11:04
     */
    @Component
    // 监听springboot容器是否启动,启动了就执行定时任务
    public class StartApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
          
          
    
        @Autowired
        private Scheduler scheduler;
    
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
          
          
    
            try {
          
          
                TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "group1");
                /**
                 * 在调度器中获取指定key的trigger,
                 */
                Trigger trigger = scheduler.getTrigger(triggerKey);
                if(trigger == null){
          
          
                    trigger = TriggerBuilder.newTrigger()
                            .withIdentity("trigger1","group1")
                            .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                            .startNow()
                            .build();
                }
                JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class).withIdentity("job1", "group1").build();
                scheduler.scheduleJob(jobDetail, trigger);
    
    
                TriggerKey triggerKey2 = TriggerKey.triggerKey("trigger1", "group1");
                /**
                 * 在调度器中获取指定key的trigger,
                 */
                Trigger trigger2 = scheduler.getTrigger(triggerKey2);
                if(trigger2 == null){
          
          
                    trigger2 = TriggerBuilder.newTrigger()
                            .withIdentity("trigger2","group2")
                            .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
                            .startNow()
                            .build();
                }
                JobDetail jobDetail2 = JobBuilder.newJob(QuartzJob.class).withIdentity("job2", "group2").build();
                scheduler.scheduleJob(jobDetail2, trigger2);
                scheduler.start();
            } catch (SchedulerException e) {
          
          
                e.printStackTrace();
            }
        }
    }
    
  • 最初にノードを開始する

    # 任务是在同样一个节点执行的
    A029095-NC1667802623784
    taskname=job1
    执行时间:Mon Nov 07 14:30:32 CST 2022
    A029095-NC1667802623784
    taskname=job2
    执行时间:Mon Nov 07 14:30:32 CST 2022
    A029095-NC1667802623784
    taskname=job1
    执行时间:Mon Nov 07 14:30:42 CST 2022
    A029095-NC1667802623784
    taskname=job2
    执行时间:Mon Nov 07 14:30:42 CST 2022
    
  • ポート番号を変更してノードを起動する

    最初のノード

    A029095-NC1667802623784
    taskname=job1
    执行时间:Mon Nov 07 14:31:42 CST 2022
    A029095-NC1667802623784
    taskname=job1
    执行时间:Mon Nov 07 14:31:52 CST 2022
    

    2 番目のノード

    A029095-NC1667802696272
    taskname=job2
    执行时间:Mon Nov 07 14:31:42 CST 2022
    A029095-NC1667802696272
    taskname=job2
    执行时间:Mon Nov 07 14:31:52 CST 2022
    

v

おすすめ

転載: blog.csdn.net/qq_45408390/article/details/127732350