1. 程式人生 > >使用Quartz實現定時任務

使用Quartz實現定時任務

一:Quertz的用途

  Quertz是一個開源的作業任務排程框架,他可以完成像JavaScript定時器類式的功能,其實Java中Timer也可實現部分功能,但相比Quertz還是略遜一籌,本人這次需要解決的就是定期統計消費記錄的功能。你還可以用他完成定期執行各類操作的功能。比如

    • 想每月25號,信用卡自動還款
    • 想每年4月1日自己給當年暗戀女神發一封匿名賀卡
    • 想每隔1小時,備份一下自己的學習筆記到雲盤

這些問題總結起來就是:

在某一個有規律的時間點幹某件事。並且時間的觸發的條件可以非常複雜(比如每月最後一個工作日的17:50),複雜到需要一個專門的框架來幹這個事。

Quartz就是來幹這樣的事,你給它一個觸發條件的定義,它負責到了時間點,觸發相應的Job起來幹活。

二:使用方法(主要是Spring整合使用)

採用Spring整合Quartz使用程式碼方式或者xml方式都可以,我這裡也提供兩種方式,名稱相同適合對比學習。

程式碼方式

  1:引入配置,pom.xml檔案引入下列兩個路徑(非Maven可自行配置)

        <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <
groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support --> <dependency>
<groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.1.0.RELEASE</version> </dependency>
Pom.xml

  2:建立【com.xqc.campusshop.config.quartz】包進行相關配置

(我知道大家不喜歡看原始碼,但是我還是得說看原始碼效果好)原始碼中

productSellDailyService為定期統計消費記錄Service介面  

dailyCalculate 為ProductSellDailyService介面中執行定期統計的的方法,

triggerFactory.setCronExpression("? 0 0 * * ? *");為定時的時間,可訪問線上cron表示式生成器生成相應時間 http://cron.qqe2.com/

 1 package com.xqc.campusshop.config.quartz;
 2 
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.context.annotation.Bean;
 5 import org.springframework.context.annotation.Configuration;
 6 import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
 7 import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
 8 import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 9 
10 import com.xqc.campusshop.service.ProductSellDailyService;
11 
12 @Configuration
13 public class QuartzConfiguration {
14     
15     //定期統計消費記錄Service介面
16     @Autowired
17     private ProductSellDailyService productSellDailyService;
18     
19     @Autowired
20     private MethodInvokingJobDetailFactoryBean jobDetailFactory;
21     
22     @Autowired
23     private CronTriggerFactoryBean productSellDailyTriggerFactory;
24     
25     /**
26      * 建立jobDetail並返回
27      * @return
28      */
29     @Bean(name="jobDetailFactory")
30     public MethodInvokingJobDetailFactoryBean crateJobDetail(){
31     
32         //new出jobDetailFactory物件,此工廠主要用來製作一個jobDetail,及製作一個任務
33         //由於我們所做的定時任務根本上講其實就是執行一個方法,所以這個工廠比較方便
34         MethodInvokingJobDetailFactoryBean jobDetailFactoryBean = new MethodInvokingJobDetailFactoryBean();
35         //設定jobDetail的名字
36         jobDetailFactoryBean.setName("product_sell_daily_job");
37         //設定jobDetail的組名
38         jobDetailFactoryBean.setGroup("job_product_sell_daily_group");
39         //對於相同的JobDetail,當指定多個Triggger時,很可能第一個job完成以前,第二個job就開始了
40         //指定設為false,多個job則不會併發執行,第二個job不會再第一個job完成前開始
41         jobDetailFactoryBean.setConcurrent(false);
42         //指定執行任務的類
43         jobDetailFactoryBean.setTargetObject(productSellDailyService);
44         //指定執行任務的方法
45         jobDetailFactoryBean.setTargetMethod("dailyCalculate");
46         
47         return jobDetailFactoryBean;    
48     }
49     
50     /**
51      * 建立cronTriggerFactory並返回
52      * 
53      * @return
54      */
55     @Bean("productSellDailyTriggerFactory")
56     public CronTriggerFactoryBean createProductSellDailyTrigger(){
57         //建立TriggerFactory例項,用來建立trigger
58         CronTriggerFactoryBean triggerFactory = new CronTriggerFactoryBean();
59         //設定triggerFactory的名字
60         triggerFactory.setName("product_sell_daily_trigger");
61         //設定組名
62         triggerFactory.setGroup("job_product_sell_daily_group");
63         //繫結jobDetail
64         triggerFactory.setJobDetail(jobDetailFactory.getObject());
65         //設定cron表示式,請訪問:http://cron.qqe2.com/線上表示式生成器
66         triggerFactory.setCronExpression("? 0 0 * * ? *");
67         
68         return triggerFactory;
69         
70     }
71     /**
72      * 建立排程工廠並返回
73      * @return
74      */
75     @Bean("schedulerFactory")
76     public SchedulerFactoryBean createSchedulerFactory(){
77         
78         SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
79         schedulerFactory.setTriggers(productSellDailyTriggerFactory.getObject());
80         return schedulerFactory;
81         
82     }
83     
84 
85 }

3:業務方法我就不貼了,大家可以列印一下測試一下即可(記得把cron表示式時間改小一點),譬如

 1 package com.xqc.campusshop.service.impl;
 2 
 3 import org.springframework.stereotype.Service;
 4 
 5 @Service
 6 public class ProductSellDailyServiceImpl implements ProductSellDailyService{
 7     
 8     @Override
 9     public void dailyCalculate() {
10             system.out.println("Quartz跑起來了!”);
11         
12     }    
13 }

使用配置檔案方式

1:引入pom.xml(同上)

2:在Spring配置檔案配置如下

<!-- 使用MethodInvokingJobDetailFactoryBean,任務類可以不實現Job介面,通過targetMethod指定呼叫方法-->
    <bean id="productSellDailyService" class="com.xqc.campusshop.service"/>
    <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="group" value="job_product_sell_daily_group"/>
        <property name="name" value="product_sell_daily_job"/>
        <!--false表示等上一個任務執行完後再開啟新的任務-->
        <property name="concurrent" value="false"/>
        <property name="targetObject">
            <ref bean="productSellDailyService"/>
        </property>
        <property name="targetMethod">
            <value>dailyCalculate</value>
        </property>
    </bean>
    <!--  排程觸發器 -->
    <bean id="myTrigger"
        class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="name" value="product_sell_daily_trigger"/>
        <property name="group" value="job_product_sell_daily_group"/>
        <property name="jobDetail">
            <ref bean="jobDetail" />
        </property>
        <property name="cronExpression">
            <value>? 0 0 * * ? *</value>
        </property>
    </bean>
    <!-- 排程工廠 -->
    <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="myTrigger"/>
            </list>
        </property>
    </bean>

3:編寫業務方法(同上)

三:相關知識

三個核心概念:

Scheduler:排程器。所有的排程都是由它控制。

Trigger: 定義觸發的條件。

 Job & JobDetail: JobDetail 定義的是任務資料,而真正的執行邏輯是在Job中,為什麼設計成 JobDetail + Job,不直接使用Job?這是因為任務是有可能併發執行,如果Scheduler直接使用Job,就會存在對同一個Job例項併發訪問的問題。而JobDetail & Job 方式,sheduler每次執行,都會根據JobDetail建立一個新的Job例項,這樣就可以規避併發訪問的問題。

  核心關係圖

Quartz體系結構

重要組成部分

Job

Job例項在Quartz中的生命週期

每次排程器執行Job時,它在呼叫execute方法前會建立一個新的Job例項

當呼叫完成後,關聯的Job物件例項會被釋放,釋放例項會被垃圾回收機制回收。

JobBuild

JonDetail

JobDetail為Job例項提供了許多設定屬性,以及JobDataMap成員變數屬性,它用來儲存特定Job例項資訊,可以理解為Job攜帶的內容。排程器需要藉助JobDetail物件來新增Job例項

JobDetail和Trigger都有namegroup

name是它們在這個sheduler裡面的唯一標識。如果我們要更新一個JobDetail定義,只需要設定一個name相同的JobDetail例項即可。

group是一個組織單元,sheduler會提供一些對整組操作的API,比如 scheduler.resumeJobs()。

JobExecutionContext

當Scheduler呼叫一個Job,就會將JobExecutionContext傳遞給Job的execute()方法

Job能通過JobExecutionContext物件訪問到Quartz執行時候的環境以及Job本身的明細資料

JobDataMap

在進行任務排程時JobDataMap儲存,在JobExecutionContext中,非常方便獲取

JobDataMap可以用來裝載任何可序列化的資料物件,當Job例項物件被執行時這些引數物件會傳遞給他

JobDataMap實現了JDK的Map介面,並且添加了一些非常方便的方法用來存取基本的資料型別

獲取JobDataMap的兩種方式

從Map中直接獲取

Job實現類中新增Setter方法對應JobDataMap的鍵值(Quartz框架預設的JobFactory實現類在初始化Job例項物件時就會自動呼叫這些setter方法

Trigger

startTimeendTime指定的Trigger會被觸發的時間區間。在這個區間之外,Trigger是不會被觸發的。

Trigger的實現類

SimpleTrigger

指定從某一個時間開始,以一定的時間間隔(單位是毫秒)執行的任務。

它適合的任務類似於:9:00 開始,每隔1小時,執行一次。

它的屬性有:

repeatInterval 重複間隔

repeatCount 重複次數。實際執行次數是 repeatCount+1。因為在startTime的時候一定會執行一次。

CronTrigger

適合於更復雜的任務,它支援型別於Linux Cron的語法(並且更強大)。基本上它覆蓋了以上三個Trigger的絕大部分能力(但不是全部)—— 當然,也更難理解。

它適合的任務類似於:每天0:00,9:00,18:00各執行一次。

它的屬性只有:

    • Cron表示式。雖然有線上生成器,但是還是介紹一下

星號():可用在所有欄位中,表示對應時間域的每一個時刻,例如, 在分鐘欄位時,表示“每分鐘”;

問號(?):該字元只在日期和星期欄位中使用,它通常指定為“無意義的值”,相當於點位符;

減號(-):表達一個範圍,如在小時欄位中使用“10-12”,則表示從10到12點,即10,11,12;

逗號(,):表達一個列表值,如在星期欄位中使用“MON,WED,FRI”,則表示星期一,星期三和星期五;

斜槓(/):x/y表達一個等步長序列,x為起始值,y為增量步長值。如在分鐘欄位中使用0/15,則表示為0,15,30和45秒,而5/15在分鐘欄位中表示5,20,35,50,你也可以使用*/y,它等同於0/y;

L:該字元只在日期和星期欄位中使用,代表“Last”的意思,但它在兩個欄位中意思不同。L在日期欄位中,表示這個月份的最後一天,如一月的31號,非閏年二月的28號;如果L用在星期中,則表示星期六,等同於7。但是,如果L出現在星期欄位裡,而且在前面有一個數值X,則表示“這個月的最後X天”,例如,6L表示該月的最後星期五;

W:該字元只能出現在日期欄位裡,是對前導日期的修飾,表示離該日期最近的工作日。例如15W表示離該月15號最近的工作日,如果該月15號是星期六,則匹配14號星期五;如果15日是星期日,則匹配16號星期一;如果15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不能夠跨月,如你指定1W,如果1號是星期六,結果匹配的是3號星期一,而非上個月最後的那天。W字串只能指定單一日期,而不能指定日期範圍;

LW組合:在日期欄位可以組合使用LW,它的意思是當月的最後一個工作日;

井號(#):該字元只能在星期欄位中使用,表示當月某個工作日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;

C:該字元只在日期和星期欄位中使用,代表“Calendar”的意思。它的意思是計劃所關聯的日期,如果日期沒有被關聯,則相當於日曆中所有日期。例如5C在日期欄位中就相當於日曆5日以後的第一天。1C在星期欄位中相當於星期日後的第一天。

JobStore

 Quartz支援任務持久化,這可以讓你在執行時增加任務或者對現存的任務進行修改,併為後續任務的執行持久化這些變更和增加的部分。中心概念是JobStore介面。預設的是RAMJobStore。

ThreadTool

TriggerBuild

Scheduler

Calendar

Quartz體貼地為我們提供以下幾種Calendar,注意,所有的Calendar既可以是排除,也可以是包含,取決於:

HolidayCalendar。指定特定的日期,比如20140613。精度到天。

DailyCalendar。指定每天的時間段(rangeStartingTime, rangeEndingTime),格式是HH:MM[:SS[:mmm]]。也就是最大精度可以到毫秒。

WeeklyCalendar。指定每星期的星期幾,可選值比如為java.util.Calendar.SUNDAY。精度是天。

MonthlyCalendar。指定每月的幾號。可選值為1-31。精度是天

AnnualCalendar。 指定每年的哪一天。使用方式如上例。精度是天。

CronCalendar。指定Cron表示式。精度取決於Cron表示式,也就是最大精度可以到秒。

一個Trigger可以和多個Calendar關聯,以排除或包含某些時間點

監聽器:

JobListener,TriggerListener,SchedulerListener

原生執行過程:

通過工廠建立一個Scheduler

建立一個實現Job介面的實現類(就是要具體做的事情,可以具體呼叫自己寫的service)

定義一個Job,並繫結我們自己實現Job介面的實現類(例如通過JobBuilder的方式)

建立Trigger,並設定相關引數,如啟動時間等。

將job和trigger繫結到scheduler物件上,並啟動

程式碼略(網上一百度都有的HelloQuartz,我就懶得寫了)

四:深入探究

原理:

quartz定時排程是通過Object.wait方式(native方法)實現的,其本質是通過作業系統的時鐘來實現的。

設計模式:

Bulild模式

元件模式

Factory模式

鏈式寫法

 執行緒檢視

在 Quartz 中,有兩類執行緒,Scheduler 排程執行緒和任務執行執行緒,其中任務執行執行緒通常使用一個執行緒池維護一組執行緒。

  Scheduler 排程執行緒主要有兩個: 執行常規排程的執行緒,和執行 misfired trigger 的執行緒。常規排程執行緒輪詢儲存的所有 trigger,如果有需要觸發的 trigger,即到達了下一次觸發的時間,則從任務執行執行緒池獲取一個空閒執行緒,執行與該 trigger 關聯的任務。Misfire 執行緒是掃描所有的 trigger,檢視是否有 misfired trigger,如果有的話根據 misfire 的策略分別處理。下圖描述了這兩個執行緒的基本流程:

啟動流程

  若quartz是配置在spring中,當伺服器啟動時,就會裝載相關的bean。SchedulerFactoryBean實現了InitializingBean介面,因此在初始化bean的時候,會執行afterPropertiesSet方法,該方法將會呼叫SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)建立Scheduler。SchedulerFactory在建立quartzScheduler的過程中,將會讀取配置引數,初始化各個元件

叢集配置

  quartz叢集是通過資料庫表來感知其他的應用的,各個節點之間並沒有直接的通訊。只有使用持久的JobStore才能完成Quartz叢集。資料庫表:以前有12張表,現在只有11張表,現在沒有儲存listener相關的表,多了QRTZ_SIMPROP_TRIGGERS表:

QRTZ_LOCKS就是Quartz叢集實現同步機制的行鎖表,包括以下幾個鎖:CALENDAR_ACCESS 、JOB_ACCESS、MISFIRE_ACCESS 、STATE_ACCESS 、TRIGGER_ACCESS。

(篇幅有點長了,其他的以後再出吧!)